Compare commits

...

54 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
  1. 5
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 2
      .github/ISSUE_TEMPLATE/question.md
  3. 12
      .github/pull_request_template.md
  4. 52
      .github/workflows/build.yml
  5. 21
      .github/workflows/deploy.yml
  6. 2
      .github/workflows/emulator_script.sh
  7. 25
      .github/workflows/snapshot.yml
  8. 23
      .run/runAndroidTests.run.xml
  9. 23
      .run/runUnitTests.run.xml
  10. 2
      LICENSE
  11. 5
      README.md
  12. 29
      build.gradle
  13. 30
      build.gradle.kts
  14. 270
      cameraview/build.gradle
  15. 137
      cameraview/build.gradle.kts
  16. 36
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java
  17. 8
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  18. 48
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  19. 76
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java
  20. 17
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  21. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java
  22. 24
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java
  23. 32
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java
  24. 20
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/ByteBufferFrameManagerTest.java
  25. 17
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/FrameManagerTest.java
  26. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CamcorderProfilesTest.java
  27. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CropHelperTest.java
  28. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/OrientationHelperTest.java
  29. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/RotationHelperTest.java
  30. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/WorkerHandlerTest.java
  31. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
  32. 36
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java
  33. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
  34. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
  35. 3
      cameraview/src/main/AndroidManifest.xml
  36. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java
  37. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  38. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java
  39. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  40. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java
  41. 205
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  42. 38
      cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java
  43. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/AudioCodec.java
  44. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java
  45. 116
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  46. 123
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  47. 51
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java
  48. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  49. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/LogAction.java
  50. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java
  51. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera1Options.java
  52. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera2Options.java
  53. 250
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java
  54. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java
  55. 103
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java
  56. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java
  57. 152
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java
  58. 1
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/SimpleFilter.java
  59. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java
  60. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java
  61. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java
  62. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java
  63. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java
  64. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java
  65. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java
  66. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java
  67. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java
  68. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java
  69. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java
  70. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java
  71. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java
  72. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java
  73. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java
  74. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/frame/ByteBufferFrameManager.java
  75. 50
      cameraview/src/main/java/com/otaliastudios/cameraview/frame/Frame.java
  76. 31
      cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java
  77. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java
  78. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/CamcorderProfiles.java
  79. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/CropHelper.java
  80. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java
  81. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/ExifHelper.java
  82. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/FpsRangeValidator.java
  83. 97
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlTextureDrawer.java
  84. 96
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java
  85. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/Issue514Workaround.java
  86. 53
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/OrientationHelper.java
  87. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/Pool.java
  88. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/RotationHelper.java
  89. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/WorkerHandler.java
  90. 244
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java
  91. 364
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java
  92. 103
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java
  93. 80
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java
  94. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/Overlay.java
  95. 35
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayDrawer.java
  96. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java
  97. 101
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java
  98. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java
  99. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java
  100. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java
  101. Some files were not shown because too many files have changed in this diff Show More

@ -13,11 +13,12 @@ Please add a clear description of what the bug is, **and** fill the list below.
- Camera engine used: *camera1/camera2/both*
- Reproducible in official demo app: *yes/no*
- Device / Android version: *Pixel, API 28*
- I have read the [FAQ page](https://natario1.github.io/CameraView/about/faq): *yes/no*
### To Reproduce
Steps to reproduce the behavior, possibly in the demo app:
1. Go to '...'
2. Click on '....'
2. Click on '...'
3. See error
### Expected behavior
@ -38,7 +39,7 @@ Part of the XML layout with the CameraView declaration, so we can read its attri
If applicable, add screenshots to help explain your problem.
### 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.

@ -8,7 +8,7 @@ assignees: ''
---
### 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.
### Version used

@ -1,10 +1,14 @@
### Before you go
Unless this is a simple fix (typos, bugs with obvious solution), please open an issue first.
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.
Unless this is a simple fix (typos, bugs with obvious solution), please open an issue first so that
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*)
- Docs updated: ... (*yes/no*)
### Solution
If applicable, briefly describe how the issue was addressed.
If applicable, describe briefly how the issue was addressed.

@ -4,39 +4,41 @@ name: Build
on:
push:
branches:
- master
- main
pull_request:
env:
TRAVIS: true
jobs:
ANDROID_BASE_CHECKS:
name: Base Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
- uses: actions/setup-java@v2
with:
java-version: 1.8
java-version: 11
distribution: temurin
cache: gradle
- name: Perform base checks
run: ./gradlew demo:assembleDebug cameraview:javadoc
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@v1
- uses: actions/setup-java@v2
with:
java-version: 1.8
java-version: 11
distribution: temurin
cache: gradle
- name: Execute unit tests
run: ./gradlew cameraview:testDebugUnitTest
run: ./gradlew cameraview:runUnitTests --stacktrace
- name: Upload unit tests artifact
uses: actions/upload-artifact@v1
with:
name: unit_tests
path: ./cameraview/build/jacoco/
path: ./cameraview/build/coverage_input/unit_tests
ANDROID_EMULATOR_TESTS:
name: Emulator Tests
runs-on: macOS-latest
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
@ -60,56 +62,58 @@ jobs:
EMULATOR_ARCH: x86
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
- uses: actions/setup-java@v2
with:
java-version: 1.8
java-version: 11
distribution: temurin
cache: gradle
- name: Execute emulator tests
timeout-minutes: 30
uses: reactivecircus/android-emulator-runner@v2.2.0
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
emulator-build: 6031357
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/outputs/code_coverage/debugAndroidTest/connected
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@v1
- uses: actions/setup-java@v2
with:
java-version: 1.8
java-version: 11
distribution: temurin
cache: gradle
- name: Download unit tests artifact
uses: actions/download-artifact@v1
with:
name: unit_tests
path: ./cameraview/build/jacoco/
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/outputs/code_coverage/debugAndroidTest/connected
path: ./cameraview/build/coverage_input/android_tests
- name: Create merged coverage report
run: ./gradlew cameraview:mergeCoverageReports
run: ./gradlew cameraview:computeCoverage
- name: Upload merged coverage report (GitHub)
uses: actions/upload-artifact@v1
with:
name: report
path: ./cameraview/build/reports/mergedCoverageReport
path: ./cameraview/build/coverage_output/xml
- name: Upload merged coverage report (Codecov)
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_KEY }}
file: ./cameraview/build/reports/mergedCoverageReport/*
file: ./cameraview/build/coverage_output/xml/*
fail_ci_if_error: true

@ -4,17 +4,20 @@ on:
release:
types: [published]
jobs:
BINTRAY_UPLOAD:
name: Bintray Upload
MAVEN_UPLOAD:
name: Maven Upload
runs-on: ubuntu-latest
env:
TRAVIS: true
BINTRAY_USER: ${{ secrets.BINTRAY_USER }}
BINTRAY_KEY: ${{ secrets.BINTRAY_KEY }}
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@v1
- uses: actions/setup-java@v2
with:
java-version: 1.8
- name: Perform bintray upload
run: ./gradlew bintrayUpload
java-version: 11
distribution: temurin
cache: gradle
- name: Perform maven upload
run: ./gradlew publishToSonatype

@ -11,4 +11,4 @@ ADB_TAGS="$ADB_TAGS MediaEncoderEngine:I MediaEncoder:I AudioMediaEncoder:I Vide
ADB_TAGS="$ADB_TAGS CameraIntegrationTest:I MessageQueue:W MPEG4Writer:I"
adb logcat -c
adb logcat $ADB_TAGS *:E -v color &
./gradlew cameraview:connectedCheck
./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,7 +1,7 @@
Copyrights for portions of project CameraView are held by WonderKiln, Inc as
part of project CameraKit-Android (https://github.com/wonderkiln/CameraKit-Android).
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.
------------------------------------------------------------------------------

@ -1,5 +1,5 @@
[![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)
[![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)
@ -22,7 +22,7 @@ CameraView is a well documented, high-level library that makes capturing picture
addressing most of the common issues and needs, and still leaving you with flexibility where needed.
```groovy
api 'com.otaliastudios:cameraview:2.6.0'
api 'com.otaliastudios:cameraview:2.7.2'
```
- Fast & reliable
@ -129,6 +129,7 @@ Using CameraView is extremely simple:
app:cameraEngine="camera1|camera2"
app:cameraPreview="glSurface|surface|texture"
app:cameraPreviewFrameRate="@integer/preview_frame_rate"
app:cameraPreviewFrameRateExact="false|true"
app:cameraFacing="back|front"
app:cameraHdr="on|off"
app:cameraFlash="on|auto|torch|off"

@ -1,29 +0,0 @@
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
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 = 29
minSdkVersion = 15
targetSdkVersion = 29
}
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,270 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
// Required by bintray
version = '2.6.0'
group = 'com.otaliastudios'
//region android dependencies
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName project.version
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArgument "filter", "" +
"com.otaliastudios.cameraview.tools.SdkExcludeFilter," +
"com.otaliastudios.cameraview.tools.SdkIncludeFilter"
}
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.1.0'
api 'androidx.lifecycle:lifecycle-common:2.1.0'
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
def isCI = System.getenv("TRAVIS")
if (isCI) {
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 createCoverageReports() {
dependsOn "testDebugUnitTest"
dependsOn "connectedCheck"
}
task mergeCoverageReports(type: JacocoReport) {
// Let this be called without running the tests.
// dependsOn "createCoverageReports"
// However, ensure we have the compiled .class files.
dependsOn "compileDebugSources"
// 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 (isCI) {
// All these classes are tested by the integration tests that we are not able to
// run on the CI 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/engine/action/**.*')
// classFilter.add('**/com/otaliastudios/cameraview/engine/lock/**.*')
// classFilter.add('**/com/otaliastudios/cameraview/engine/meter/**.*')
// classFilter.add('**/com/otaliastudios/cameraview/picture/**.*')
// classFilter.add('**/com/otaliastudios/cameraview/video/**.*')
// classFilter.add('**/com/otaliastudios/cameraview/orchestrator/**.*')
// 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
// 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"))
}

@ -1,32 +1,13 @@
package com.otaliastudios.cameraview;
import android.opengl.EGL14;
import android.graphics.Canvas;
import com.otaliastudios.opengl.core.EglCore;
import com.otaliastudios.opengl.surface.EglOffscreenSurface;
import com.otaliastudios.opengl.surface.EglSurface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.internal.egl.EglBaseSurface;
import com.otaliastudios.cameraview.internal.egl.EglCore;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.overlay.OverlayDrawer;
import com.otaliastudios.cameraview.size.Size;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@SuppressWarnings("WeakerAccess")
@ -36,19 +17,18 @@ public abstract class BaseEglTest extends BaseTest {
protected final static int HEIGHT = 100;
protected EglCore eglCore;
protected EglBaseSurface eglSurface;
protected EglSurface eglSurface;
@Before
public void setUp() {
eglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
eglSurface = new EglBaseSurface(eglCore);
eglSurface.createOffscreenSurface(WIDTH, HEIGHT);
eglCore = new EglCore(EGL14.EGL_NO_CONTEXT, EglCore.FLAG_RECORDABLE);
eglSurface = new EglOffscreenSurface(eglCore, WIDTH, HEIGHT);
eglSurface.makeCurrent();
}
@After
public void tearDown() {
eglSurface.releaseEglSurface();
eglSurface.release();
eglSurface = null;
eglCore.release();
eglCore = null;

@ -248,7 +248,13 @@ public class CameraViewCallbacksTest extends BaseTest {
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
public void testCameraError() {

@ -19,6 +19,7 @@ import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.ControlParser;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing;
@ -54,6 +55,7 @@ import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.size.SizeSelector;
import com.otaliastudios.cameraview.size.SizeSelectors;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.After;
import org.junit.Before;
@ -165,6 +167,7 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getHdr(), controls.getHdr());
assertEquals(cameraView.getAudio(), controls.getAudio());
assertEquals(cameraView.getVideoCodec(), controls.getVideoCodec());
assertEquals(cameraView.getAudioCodec(), controls.getAudioCodec());
assertEquals(cameraView.getPictureFormat(), controls.getPictureFormat());
//noinspection SimplifiableJUnitAssertion
assertEquals(cameraView.getLocation(), null);
@ -177,6 +180,7 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getFrameProcessingMaxWidth(), 0);
assertEquals(cameraView.getFrameProcessingMaxHeight(), 0);
assertEquals(cameraView.getFrameProcessingFormat(), 0);
assertFalse(cameraView.getPreviewFrameRateExact());
// Self managed
GestureParser gestures = new GestureParser(empty);
@ -213,6 +217,17 @@ public class CameraViewTest extends BaseTest {
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
public void testGesture_enablingDisablingLayouts() {
cameraView.clearGesture(Gesture.TAP);
@ -772,6 +787,19 @@ public class CameraViewTest extends BaseTest {
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
public void testVideoCodec() {
cameraView.set(VideoCodec.H_263);
@ -833,6 +861,14 @@ public class CameraViewTest extends BaseTest {
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);
@ -878,6 +914,14 @@ public class CameraViewTest extends BaseTest {
cameraView.setFrameProcessingExecutors(0);
}
@Test
public void testDrawHardwareOverlays() {
cameraView.setDrawHardwareOverlays(true);
assertTrue(cameraView.getDrawHardwareOverlays());
cameraView.setDrawHardwareOverlays(false);
assertFalse(cameraView.getDrawHardwareOverlays());
}
//endregion
//region Lists of listeners and processors
@ -1000,6 +1044,8 @@ public class CameraViewTest extends BaseTest {
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
public void testOverlays_addOverlayView() {
cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
@ -1024,6 +1070,8 @@ public class CameraViewTest extends BaseTest {
verify(cameraView.mOverlayLayout, never()).addView(overlay, params);
}
// Broke in 31 for some reason, no time to investigate but looks like a spy() issue.
@SdkExclude(minSdkVersion = 31)
@Test
public void testOverlays_removeOverlayView() {
// First add one.

@ -4,6 +4,7 @@ package com.otaliastudios.cameraview;
import android.location.Location;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.size.Size;
@ -16,6 +17,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mockito;
import java.io.File;
import java.io.FileDescriptor;
import static org.junit.Assert.assertEquals;
@ -27,11 +29,12 @@ public class VideoResultTest extends BaseTest {
private VideoResult.Stub stub = new VideoResult.Stub();
@Test
public void testResult() {
public void testResultWithFile() {
File file = Mockito.mock(File.class);
int rotation = 90;
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);
boolean isSnapshot = true;
int maxDuration = 1234;
@ -46,7 +49,8 @@ public class VideoResultTest extends BaseTest {
stub.file = file;
stub.rotation = rotation;
stub.size = size;
stub.videoCodec = codec;
stub.videoCodec = videoCodec;
stub.audioCodec = audioCodec;
stub.location = location;
stub.isSnapshot = isSnapshot;
stub.maxDuration = maxDuration;
@ -62,7 +66,8 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getFile(), file);
assertEquals(result.getRotation(), rotation);
assertEquals(result.getSize(), size);
assertEquals(result.getVideoCodec(), codec);
assertEquals(result.getVideoCodec(), videoCodec);
assertEquals(result.getAudioCodec(), audioCodec);
assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize);
@ -73,6 +78,69 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getAudioBitRate(), audioBitRate);
assertEquals(result.getAudio(), audio);
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();
}
}

@ -31,7 +31,7 @@ import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.size.SizeSelectors;
import com.otaliastudios.cameraview.tools.Emulator;
import com.otaliastudios.cameraview.tools.Op;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.tools.Retry;
@ -45,7 +45,6 @@ import androidx.test.rule.GrantPermissionRule;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentMatcher;
@ -1081,6 +1080,20 @@ public abstract class CameraIntegrationTest<E extends CameraBaseEngine> extends
assert15Frames(processor);
}
@Test
@Retry(emulatorOnly = true)
@SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
public void testFrameProcessing_afterPicture() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor);
openSync(true);
camera.takePicture();
waitForPictureResult(true);
assert15Frames(processor);
}
@Test
@Retry(emulatorOnly = true)
@SdkExclude(maxSdkVersion = 22, emulatorOnly = true)

@ -7,6 +7,7 @@ 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.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.PictureFormat;
@ -244,10 +245,12 @@ public class Camera1OptionsTest extends BaseTest {
Collection<Grid> grids = o.getSupportedControls(Grid.class);
Collection<VideoCodec> video = o.getSupportedControls(VideoCodec.class);
Collection<AudioCodec> audioCodecs = o.getSupportedControls(AudioCodec.class);
Collection<Mode> sessions = o.getSupportedControls(Mode.class);
Collection<Audio> audio = o.getSupportedControls(Audio.class);
assertEquals(grids.size(), Grid.values().length);
assertEquals(video.size(), VideoCodec.values().length);
assertEquals(audioCodecs.size(), AudioCodec.values().length);
assertEquals(sessions.size(), Mode.values().length);
assertEquals(audio.size(), Audio.values().length);
}

@ -8,16 +8,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
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.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@ -91,11 +91,11 @@ public class BaseFilterTest extends BaseEglTest {
@Test
public void testOnProgramCreate() {
filter = new TestFilter();
int handle = GlUtils.createProgram(filter.getVertexShader(), filter.getFragmentShader());
int handle = GlTextureProgram.create(filter.getVertexShader(), filter.getFragmentShader());
filter.onCreate(handle);
assertTrue(filter.programHandle >= 0);
assertNotNull(filter.program);
filter.onDestroy();
assertTrue(filter.programHandle < 0);
assertNull(filter.program);
GLES20.glDeleteProgram(handle);
}
@ -111,18 +111,18 @@ public class BaseFilterTest extends BaseEglTest {
@Test
public void testDraw() {
// Use an EglViewport which cares about GL setup.
// Use a drawer which cares about GL setup.
filter = spy(new TestFilter());
EglViewport viewport = new EglViewport(filter);
int texture = viewport.createTexture();
GlTextureDrawer drawer = new GlTextureDrawer();
drawer.setFilter(filter);
float[] matrix = new float[16];
viewport.drawFrame(0L, texture, matrix);
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);
viewport.release();
drawer.release();
}
@Test(expected = RuntimeException.class)

@ -11,8 +11,8 @@ 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.GlUtils;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.opengl.program.GlProgram;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -127,7 +127,7 @@ public class MultiFilterTest extends BaseEglTest {
DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter);
int program = GlUtils.createProgram(multiFilter.getVertexShader(),
int program = GlProgram.create(multiFilter.getVertexShader(),
multiFilter.getFragmentShader());
multiFilter.onCreate(program);
verify(filter, never()).onCreate(anyInt());
@ -142,11 +142,11 @@ public class MultiFilterTest extends BaseEglTest {
DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter);
multiFilter.setSize(WIDTH, HEIGHT);
EglViewport viewport = new EglViewport(multiFilter);
int texture = viewport.createTexture();
float[] matrix = new float[16];
viewport.drawFrame(0L, texture, matrix);
viewport.release();
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();
@ -165,7 +165,9 @@ public class MultiFilterTest extends BaseEglTest {
final DuotoneFilter filter2 = spy(new DuotoneFilter());
final MultiFilter multiFilter = new MultiFilter(filter1, filter2);
multiFilter.setSize(WIDTH, HEIGHT);
float[] matrix = new float[16];
GlTextureDrawer drawer = new GlTextureDrawer();
drawer.setFilter(multiFilter);
float[] matrix = drawer.getTextureTransform();
final int[] result = new int[1];
doAnswer(new Answer() {
@ -173,11 +175,11 @@ public class MultiFilterTest extends BaseEglTest {
public Object answer(InvocationOnMock invocation) {
MultiFilter.State state = multiFilter.states.get(filter1);
assertNotNull(state);
assertTrue(state.isCreated);
assertTrue(state.isProgramCreated);
assertTrue(state.isFramebufferCreated);
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
assertTrue(result[0] != 0);
// assertTrue(result[0] != 0);
return null;
}
}).when(filter1).draw(0L, matrix);
@ -189,7 +191,7 @@ public class MultiFilterTest extends BaseEglTest {
// The last filter has no FBO / texture.
MultiFilter.State state = multiFilter.states.get(filter2);
assertNotNull(state);
assertTrue(state.isCreated);
assertTrue(state.isProgramCreated);
assertFalse(state.isFramebufferCreated);
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
@ -199,10 +201,8 @@ public class MultiFilterTest extends BaseEglTest {
}
}).when(filter2).draw(eq(0L), any(float[].class));
EglViewport viewport = new EglViewport(multiFilter);
int texture = viewport.createTexture();
viewport.drawFrame(0L, texture, matrix);
viewport.release();
drawer.draw(0L);
drawer.release();
// Verify that both are drawn.
verify(filter1, times(1)).draw(0L, matrix);

@ -7,6 +7,7 @@ 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;
@ -26,6 +27,7 @@ import static org.mockito.Mockito.verify;
@SmallTest
public class ByteBufferFrameManagerTest extends BaseTest {
private final Angles angles = new Angles();
private ByteBufferFrameManager.BufferCallback callback;
@Before
@ -41,22 +43,22 @@ public class ByteBufferFrameManagerTest extends BaseTest {
@Test
public void testAllocate() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50));
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));
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));
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes();
Frame frame1 = manager.getFrame(new byte[length], 0, 0);
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.
@ -73,12 +75,12 @@ public class ByteBufferFrameManagerTest extends BaseTest {
@Test
public void testOnFrameReleased_sameLength() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50));
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, 0);
Frame frame = manager.getFrame(picture, 0);
assertNotNull(frame);
// Release the frame and ensure that onBufferAvailable is called.
@ -90,16 +92,16 @@ public class ByteBufferFrameManagerTest extends BaseTest {
@Test
public void testOnFrameReleased_differentLength() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50));
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, 0);
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));
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.

@ -8,6 +8,7 @@ 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;
@ -18,14 +19,14 @@ 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.never;
import static org.mockito.Mockito.reset;
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.
@ -39,12 +40,12 @@ public class FrameManagerTest extends BaseTest {
return data;
}
};
manager.setUp(ImageFormat.NV21, new Size(50, 50));
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
Frame first = manager.getFrame("foo", 0, 0);
Frame first = manager.getFrame("foo", 0);
assertNotNull(first);
first.release();
Frame second = manager.getFrame("bar", 0, 0);
Frame second = manager.getFrame("bar", 0);
assertNotNull(second);
second.release();
assertEquals(first, second);
@ -62,11 +63,11 @@ public class FrameManagerTest extends BaseTest {
return data;
}
};
manager.setUp(ImageFormat.NV21, new Size(50, 50));
manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
Frame first = manager.getFrame("foo", 0, 0);
Frame first = manager.getFrame("foo", 0);
assertNotNull(first);
Frame second = manager.getFrame("bar", 0, 0);
Frame second = manager.getFrame("bar", 0);
assertNull(second);
}
}

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import android.media.CamcorderProfile;
@ -8,6 +8,7 @@ import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.internal.CamcorderProfiles;
import com.otaliastudios.cameraview.tools.SdkExclude;
import com.otaliastudios.cameraview.size.Size;

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

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -6,6 +6,7 @@ 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;

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

@ -1,9 +1,10 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.tools.Op;
import androidx.annotation.NonNull;

@ -30,7 +30,7 @@ import static org.mockito.Mockito.verify;
* Not clear why, but for some reason on API 28+ the UiThreadTests here crash for an internal NPE
* in FrameLayout.onMeasure.
*/
@SdkExclude(minSdkVersion = 28, maxSdkVersion = 29)
@SdkExclude(minSdkVersion = 28)
@TargetApi(17)
public class MarkerLayoutTest extends BaseTest {

@ -1,48 +1,29 @@
package com.otaliastudios.cameraview.overlay;
import android.content.res.XmlResourceParser;
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.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.egl.EglBaseSurface;
import com.otaliastudios.cameraview.internal.egl.EglCore;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.size.Size;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
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.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.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
@ -79,22 +60,19 @@ public class OverlayDrawerTest extends BaseEglTest {
@Test
public void testRender() {
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.render(0L);
verify(drawer.mViewport, times(1)).drawFrame(
0L,
drawer.mTextureId,
drawer.getTransform()
);
verify(drawer.mTextureDrawer, times(1)).draw(0L);
}
@Test
public void testRelease() {
OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
EglViewport viewport = spy(drawer.mViewport);
drawer.mViewport = viewport;
GlTextureDrawer textureDrawer = spy(drawer.mTextureDrawer);
drawer.mTextureDrawer = textureDrawer;
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 com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.After;
import org.junit.Before;
@ -129,6 +130,7 @@ public class OverlayLayoutTest extends BaseTest {
assertTrue(overlayLayout.drawChild(canvas, child, 0));
}
@SdkExclude(minSdkVersion = 31) // spying views does not work properly on 31, should investigate
@UiThreadTest
@Test
public void testDraw() {
@ -142,6 +144,7 @@ public class OverlayLayoutTest extends BaseTest {
verify(overlayLayout, times(1)).drawOn(Overlay.Target.PREVIEW, canvas);
}
@SdkExclude(minSdkVersion = 31) // spying views does not work properly on 31, should investigate
@UiThreadTest
@Test
public void testDrawOn() {

@ -10,7 +10,7 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.filter.Filter;
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) {
super(context, parent);

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

@ -12,7 +12,7 @@ public interface BitmapCallback {
/**
* Notifies that the bitmap was successfully decoded.
* 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
*/

@ -161,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;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Utility class that can log traces and info.
@ -59,7 +59,7 @@ public final class CameraLogger {
@VisibleForTesting static String lastTag;
private static int sLevel;
private static List<Logger> sLoggers = new ArrayList<>();
private static Set<Logger> sLoggers = new CopyOnWriteArraySet<>();
@VisibleForTesting static Logger sAndroidLogger = new Logger() {
@Override

@ -2,44 +2,30 @@ package com.otaliastudios.cameraview;
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.AudioCodec;
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.PictureFormat;
import com.otaliastudios.cameraview.controls.Preview;
import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
import com.otaliastudios.cameraview.engine.mappers.Camera2Mapper;
import com.otaliastudios.cameraview.gesture.GestureAction;
import com.otaliastudios.cameraview.controls.Grid;
import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
@ -120,6 +106,8 @@ public abstract class CameraOptions {
return (Collection<T>) Arrays.asList(Mode.values());
} else if (controlClass.equals(VideoCodec.class)) {
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)) {
return (Collection<T>) getSupportedWhiteBalance();
} else if (controlClass.equals(Engine.class)) {

@ -11,8 +11,8 @@ import android.os.Handler;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
import com.otaliastudios.cameraview.internal.utils.ExifHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.internal.ExifHelper;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -94,6 +94,7 @@ public class CameraUtils {
stream.flush();
return file;
} catch (IOException e) {
LOG.e("writeToFile:", "could not write file.", e);
return null;
}
}

@ -33,6 +33,7 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.controls.ControlParser;
import com.otaliastudios.cameraview.controls.Engine;
@ -66,8 +67,8 @@ import com.otaliastudios.cameraview.gesture.PinchGestureFinder;
import com.otaliastudios.cameraview.gesture.ScrollGestureFinder;
import com.otaliastudios.cameraview.gesture.TapGestureFinder;
import com.otaliastudios.cameraview.internal.GridLinesLayout;
import com.otaliastudios.cameraview.internal.utils.CropHelper;
import com.otaliastudios.cameraview.internal.utils.OrientationHelper;
import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.internal.OrientationHelper;
import com.otaliastudios.cameraview.markers.AutoFocusMarker;
import com.otaliastudios.cameraview.markers.AutoFocusTrigger;
import com.otaliastudios.cameraview.markers.MarkerLayout;
@ -86,6 +87,7 @@ import com.otaliastudios.cameraview.size.SizeSelectorParser;
import com.otaliastudios.cameraview.size.SizeSelectors;
import java.io.File;
import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -131,6 +133,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
private Engine mEngine;
private Filter mPendingFilter;
private int mFrameProcessingExecutors;
private int mActiveGestures;
// Components
private Handler mUiHandler;
@ -205,6 +208,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
int videoBitRate = a.getInteger(R.styleable.CameraView_cameraVideoBitRate, 0);
int audioBitRate = a.getInteger(R.styleable.CameraView_cameraAudioBitRate, 0);
float videoFrameRate = a.getFloat(R.styleable.CameraView_cameraPreviewFrameRate, 0);
boolean videoFrameRateExact = a.getBoolean(R.styleable.CameraView_cameraPreviewFrameRateExact, false);
long autoFocusResetDelay = (long) a.getInteger(
R.styleable.CameraView_cameraAutoFocusResetDelay,
(int) DEFAULT_AUTOFOCUS_RESET_DELAY_MILLIS);
@ -223,6 +227,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
int frameExecutors = a.getInteger(R.styleable.CameraView_cameraFrameProcessingExecutors,
DEFAULT_FRAME_PROCESSING_EXECUTORS);
boolean drawHardwareOverlays = a.getBoolean(R.styleable.CameraView_cameraDrawHardwareOverlays, false);
// Size selectors and gestures
SizeSelectorParser sizeSelectors = new SizeSelectorParser(a);
GestureParser gestures = new GestureParser(a);
@ -256,6 +262,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setUseDeviceOrientation(useDeviceOrientation);
setGrid(controls.getGrid());
setGridColor(gridColor);
setDrawHardwareOverlays(drawHardwareOverlays);
// Apply camera engine params
// Adding new ones? See setEngine().
@ -266,6 +273,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setHdr(controls.getHdr());
setAudio(controls.getAudio());
setAudioBitRate(audioBitRate);
setAudioCodec(controls.getAudioCodec());
setPictureSize(sizeSelectors.getPictureSizeSelector());
setPictureMetering(pictureMetering);
setPictureSnapshotMetering(pictureSnapshotMetering);
@ -276,6 +284,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setVideoMaxDuration(videoMaxDuration);
setVideoBitRate(videoBitRate);
setAutoFocusResetDelay(autoFocusResetDelay);
setPreviewFrameRateExact(videoFrameRateExact);
setPreviewFrameRate(videoFrameRate);
setSnapshotMaxWidth(snapshotMaxWidth);
setSnapshotMaxHeight(snapshotMaxHeight);
@ -389,12 +398,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// attached. That's why we instantiate the preview here.
doInstantiatePreview();
}
mOrientationHelper.enable();
}
@Override
protected void onDetachedFromWindow() {
if (!mInEditor) mOrientationHelper.disable();
mLastPreviewStreamSize = null;
super.onDetachedFromWindow();
}
@ -602,6 +609,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
mGestureMap.get(Gesture.SCROLL_VERTICAL) != none);
break;
}
mActiveGestures = 0;
for(GestureAction act : mGestureMap.values()) {
mActiveGestures += act == GestureAction.NONE ? 0 : 1;
}
return true;
}
mapGesture(gesture, none);
@ -630,7 +643,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true; // Steal our own events.
// Steal our own events if gestures are enabled
return mActiveGestures > 0;
}
@SuppressLint("ClickableViewAccessibility")
@ -666,6 +680,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//noinspection ConstantConditions
switch (action) {
case TAKE_PICTURE_SNAPSHOT:
takePictureSnapshot();
break;
case TAKE_PICTURE:
takePicture();
break;
@ -723,6 +741,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//region Lifecycle APIs
/**
* Sets permissions flag if you want enable auto check permissions or disable it.
* @param requestPermissions - true: auto check permissions enabled, false: auto check permissions disabled.
*/
@SuppressWarnings("unused")
public void setRequestPermissions(boolean requestPermissions) {
mRequestPermissions = requestPermissions;
}
/**
* Returns whether the camera engine has started.
* @return whether the camera has started
@ -741,12 +768,26 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* Sets the lifecycle owner for this view. This means you don't need
* to call {@link #open()}, {@link #close()} or {@link #destroy()} at all.
*
* If you want that lifecycle stopped controlling the state of the camera,
* pass null in this method.
*
* @param owner the owner activity or fragment
*/
public void setLifecycleOwner(@NonNull LifecycleOwner owner) {
if (mLifecycle != null) mLifecycle.removeObserver(this);
mLifecycle = owner.getLifecycle();
mLifecycle.addObserver(this);
public void setLifecycleOwner(@Nullable LifecycleOwner owner) {
if (owner == null) {
clearLifecycleObserver();
} else {
clearLifecycleObserver();
mLifecycle = owner.getLifecycle();
mLifecycle.addObserver(this);
}
}
private void clearLifecycleObserver() {
if (mLifecycle != null) {
mLifecycle.removeObserver(this);
mLifecycle = null;
}
}
/**
@ -828,6 +869,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void close() {
if (mInEditor) return;
mOrientationHelper.disable();
mCameraEngine.stop(false);
if (mCameraPreview != null) mCameraPreview.onPause();
}
@ -881,6 +923,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setWhiteBalance((WhiteBalance) control);
} else if (control instanceof VideoCodec) {
setVideoCodec((VideoCodec) control);
} else if (control instanceof AudioCodec) {
setAudioCodec((AudioCodec) control);
} else if (control instanceof Preview) {
setPreview((Preview) control);
} else if (control instanceof Engine) {
@ -917,6 +961,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return (T) getWhiteBalance();
} else if (controlClass == VideoCodec.class) {
return (T) getVideoCodec();
} else if (controlClass == AudioCodec.class) {
return (T) getAudioCodec();
} else if (controlClass == Preview.class) {
return (T) getPreview();
} else if (controlClass == Engine.class) {
@ -989,6 +1035,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setHdr(oldEngine.getHdr());
setAudio(oldEngine.getAudio());
setAudioBitRate(oldEngine.getAudioBitRate());
setAudioCodec(oldEngine.getAudioCodec());
setPictureSize(oldEngine.getPictureSizeSelector());
setPictureFormat(oldEngine.getPictureFormat());
setVideoSize(oldEngine.getVideoSizeSelector());
@ -998,6 +1045,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setVideoBitRate(oldEngine.getVideoBitRate());
setAutoFocusResetDelay(oldEngine.getAutoFocusResetDelay());
setPreviewFrameRate(oldEngine.getPreviewFrameRate());
setPreviewFrameRateExact(oldEngine.getPreviewFrameRateExact());
setSnapshotMaxWidth(oldEngine.getSnapshotMaxWidth());
setSnapshotMaxHeight(oldEngine.getSnapshotMaxHeight());
setFrameProcessingMaxWidth(oldEngine.getFrameProcessingMaxWidth());
@ -1542,6 +1590,38 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.getVideoBitRate();
}
/**
* A flag to control the behavior when calling {@link #setPreviewFrameRate(float)}.
*
* If the value is set to true, {@link #setPreviewFrameRate(float)} will choose the preview
* frame range as close to the desired new frame rate as possible. Which mean it may choose a
* narrow range around the desired frame rate. Note: This option will give you as exact fps as
* you want but the sensor will have less freedom when adapting the exposure to the environment,
* which may lead to dark preview.
*
* If the value is set to false, {@link #setPreviewFrameRate(float)} will choose as broad range
* as it can.
*
* @param videoFrameRateExact whether want a more exact preview frame range
*
* @see #setPreviewFrameRate(float)
*/
public void setPreviewFrameRateExact(boolean videoFrameRateExact) {
mCameraEngine.setPreviewFrameRateExact(videoFrameRateExact);
}
/**
* Returns whether we want to set preview fps as exact as we set through
* {@link #setPreviewFrameRate(float)}.
*
* @see #setPreviewFrameRateExact(boolean)
* @see #setPreviewFrameRate(float)
* @return current option
*/
public boolean getPreviewFrameRateExact() {
return mCameraEngine.getPreviewFrameRateExact();
}
/**
* Sets the preview frame rate in frames per second.
* This rate will be used, for example, by the frame processor and in video
@ -1585,6 +1665,30 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.getAudioBitRate();
}
/**
* Sets the encoder for audio recordings.
* Defaults to {@link AudioCodec#DEVICE_DEFAULT}.
*
* @see AudioCodec#DEVICE_DEFAULT
* @see AudioCodec#AAC
* @see AudioCodec#HE_AAC
* @see AudioCodec#AAC_ELD
*
* @param codec requested audio codec
*/
public void setAudioCodec(@NonNull AudioCodec codec) {
mCameraEngine.setAudioCodec(codec);
}
/**
* Gets the current encoder for audio recordings.
* @return the current audio codec
*/
@NonNull
public AudioCodec getAudioCodec() {
return mCameraEngine.getAudioCodec();
}
/**
* Adds a {@link CameraListener} instance to be notified of all
* interesting events that happen during the camera lifecycle.
@ -1646,8 +1750,28 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* @param file a file where the video will be saved
*/
public void takeVideo(@NonNull File file) {
takeVideo(file, null);
}
/**
* Starts recording a video. Video will be written to the given file,
* so callers should ensure they have appropriate permissions to write to the file.
*
* @param fileDescriptor a file descriptor where the video will be saved
*/
public void takeVideo(@NonNull FileDescriptor fileDescriptor) {
takeVideo(null, fileDescriptor);
}
private void takeVideo(@Nullable File file, @Nullable FileDescriptor fileDescriptor) {
VideoResult.Stub stub = new VideoResult.Stub();
mCameraEngine.takeVideo(stub, file);
if (file != null) {
mCameraEngine.takeVideo(stub, file, null);
} else if (fileDescriptor != null) {
mCameraEngine.takeVideo(stub, null, fileDescriptor);
} else {
throw new IllegalStateException("file and fileDescriptor are both null.");
}
mUiHandler.post(new Runnable() {
@Override
public void run() {
@ -1686,9 +1810,27 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
*
* @param file a file where the video will be saved
* @param durationMillis recording max duration
*
*/
public void takeVideo(@NonNull File file, int durationMillis) {
takeVideo(file, null, durationMillis);
}
/**
* Starts recording a video. Video will be written to the given file,
* so callers should ensure they have appropriate permissions to write to the file.
* Recording will be automatically stopped after the given duration, overriding
* temporarily any duration limit set by {@link #setVideoMaxDuration(int)}.
*
* @param fileDescriptor a file descriptor where the video will be saved
* @param durationMillis recording max duration
*/
@SuppressWarnings("unused")
public void takeVideo(@NonNull FileDescriptor fileDescriptor, int durationMillis) {
takeVideo(null, fileDescriptor, durationMillis);
}
private void takeVideo(@Nullable File file, @Nullable FileDescriptor fileDescriptor,
int durationMillis) {
final int old = getVideoMaxDuration();
addCameraListener(new CameraListener() {
@Override
@ -1707,7 +1849,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
});
setVideoMaxDuration(durationMillis);
takeVideo(file);
takeVideo(file, fileDescriptor);
}
/**
@ -2017,6 +2159,25 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.isTakingPicture();
}
/**
* Sets the overlay layout hardware canvas capture mode to allow hardware
* accelerated views to be captured in snapshots
*
* @param on true if enabled
*/
public void setDrawHardwareOverlays(boolean on) {
mOverlayLayout.setHardwareCanvasEnabled(on);
}
/**
* Returns true if the overlay layout is set to capture the hardware canvas
* of child views
*
* @return boolean indicating hardware canvas capture is enabled
*/
public boolean getDrawHardwareOverlays() {
return mOverlayLayout.getHardwareCanvasEnabled();
}
//endregion
//region Callbacks and dispatching
@ -2097,10 +2258,18 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
@Override
public void onShutter(boolean shouldPlaySound) {
public void dispatchOnPictureShutter(boolean shouldPlaySound) {
if (shouldPlaySound && mPlaySounds) {
playSound(MediaActionSound.SHUTTER_CLICK);
}
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (CameraListener listener : mListeners) {
listener.onPictureShutter();
}
}
});
}
@Override
@ -2201,12 +2370,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
@Override
public void onDisplayOffsetChanged(int displayOffset, boolean willRecreate) {
LOG.i("onDisplayOffsetChanged", displayOffset, "recreate:", willRecreate);
if (isOpened() && !willRecreate) {
// Display offset changes when the device rotation lock is off and the activity
// is free to rotate. However, some changes will NOT recreate the activity, namely
// 180 degrees flips. In this case, we must restart the camera manually.
public void onDisplayOffsetChanged() {
if (isOpened()) {
// We can't handle display offset (View angle) changes without restarting.
// See comments in OrientationHelper for more information.
LOG.w("onDisplayOffsetChanged", "restarting the camera.");
close();
open();

@ -3,15 +3,17 @@ package com.otaliastudios.cameraview;
import android.location.Location;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.size.Size;
import java.io.File;
import java.io.FileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.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 Size size;
public File file;
public FileDescriptor fileDescriptor;
public Facing facing;
public VideoCodec videoCodec;
public AudioCodec audioCodec;
public Audio audio;
public long maxSize;
public int maxDuration;
@ -55,8 +59,10 @@ public class VideoResult {
private final int rotation;
private final Size size;
private final File file;
private final FileDescriptor fileDescriptor;
private final Facing facing;
private final VideoCodec videoCodec;
private final AudioCodec audioCodec;
private final Audio audio;
private final long maxSize;
private final int maxDuration;
@ -71,8 +77,10 @@ public class VideoResult {
rotation = builder.rotation;
size = builder.size;
file = builder.file;
fileDescriptor = builder.fileDescriptor;
facing = builder.facing;
videoCodec = builder.videoCodec;
audioCodec = builder.audioCodec;
audio = builder.audio;
maxSize = builder.maxSize;
maxDuration = builder.maxDuration;
@ -130,9 +138,25 @@ public class VideoResult {
*/
@NonNull
public File getFile() {
if (file == null) {
throw new RuntimeException("File is only available when takeVideo(File) is used.");
}
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.
*
@ -153,6 +177,16 @@ public class VideoResult {
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,
* or 0 if no constraint was set.

@ -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,6 +22,7 @@ public class ControlParser {
private int hdr;
private int audio;
private int videoCodec;
private int audioCodec;
private int engine;
private int pictureFormat;
@ -38,6 +39,8 @@ public class ControlParser {
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());
@ -84,6 +87,11 @@ public class ControlParser {
return Audio.fromValue(audio);
}
@NonNull
public AudioCodec getAudioCodec() {
return AudioCodec.fromValue(audioCodec);
}
@NonNull
public VideoCodec getVideoCodec() {
return VideoCodec.fromValue(videoCodec);

@ -37,13 +37,13 @@ import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode;
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.Snapshot1PictureRecorder;
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.Size;
import com.otaliastudios.cameraview.video.Full1VideoRecorder;
@ -52,6 +52,7 @@ import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@ -99,7 +100,13 @@ public class Camera1Engine extends CameraBaseEngine implements
@NonNull
@Override
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());
for (Camera.Size size : sizes) {
Size add = new Size(size.width, size.height);
@ -158,17 +165,31 @@ public class Camera1Engine extends CameraBaseEngine implements
LOG.e("onStartEngine:", "Failed to connect. Maybe in use by another app?");
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);
// Set parameters that might have been set before the camera was opened.
LOG.i("onStartEngine:", "Applying default parameters.");
Camera.Parameters params = mCamera.getParameters();
mCameraOptions = new Camera1Options(params, mCameraId,
getAngles().flip(Reference.SENSOR, Reference.VIEW));
applyAllParameters(params);
mCamera.setParameters(params);
mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW,
Axis.ABSOLUTE)); // <- not allowed during preview
try {
Camera.Parameters params = mCamera.getParameters();
mCameraOptions = new Camera1Options(params, mCameraId,
getAngles().flip(Reference.SENSOR, Reference.VIEW));
applyAllParameters(params);
mCamera.setParameters(params);
} 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");
return Tasks.forResult(mCameraOptions);
}
@ -193,6 +214,7 @@ public class Camera1Engine extends CameraBaseEngine implements
mCaptureSize = computeCaptureSize();
mPreviewStreamSize = computePreviewStreamSize();
LOG.i("onStartBind:", "Returning");
return Tasks.forResult(null);
}
@ -208,8 +230,15 @@ public class Camera1Engine extends CameraBaseEngine implements
throw new IllegalStateException("previewStreamSize should not be null at this point.");
}
mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight());
mPreview.setDrawRotation(0);
Camera.Parameters params = mCamera.getParameters();
Camera.Parameters params;
try {
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);
@ -227,11 +256,16 @@ public class Camera1Engine extends CameraBaseEngine implements
Size pictureSize = computeCaptureSize(Mode.PICTURE);
params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight());
}
mCamera.setParameters(params);
try {
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(this); // Add ourselves
getFrameManager().setUp(PREVIEW_FORMAT, mPreviewStreamSize);
getFrameManager().setUp(PREVIEW_FORMAT, mPreviewStreamSize, getAngles());
LOG.i("onStartPreview", "Starting preview with startPreview().");
try {
@ -356,11 +390,12 @@ public class Camera1Engine extends CameraBaseEngine implements
LOG.i("onTakePictureSnapshot:", "executing.");
// Not the real size: it will be cropped to match the view ratio
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
// Actually it will be rotated and set to 0.
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio);
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 {
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio);
}
mPictureRecorder.take();
@ -396,13 +431,13 @@ public class Camera1Engine extends CameraBaseEngine implements
@Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub,
@NonNull AspectRatio outputRatio) {
if (!(mPreview instanceof 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) {
throw new IllegalStateException("Video snapshots are only supported on API 18+.");
}
GlCameraPreview glPreview = (GlCameraPreview) mPreview;
RendererCameraPreview glPreview = (RendererCameraPreview) mPreview;
Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null.");
@ -424,8 +459,7 @@ public class Camera1Engine extends CameraBaseEngine implements
LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size);
// Start.
mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview,
getOverlay(), stub.rotation);
mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, getOverlay());
mVideoRecorder.start(stub);
}
@ -590,7 +624,9 @@ public class Camera1Engine extends CameraBaseEngine implements
public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) {
final float old = mZoomValue;
mZoomValue = zoom;
mZoomTask = getOrchestrator().scheduleStateful("zoom (" + zoom + ")",
// Zoom requests can be high frequency (e.g. linked to touch events), let's trim the oldest.
getOrchestrator().trim("zoom", ALLOWED_ZOOM_OPS);
mZoomTask = getOrchestrator().scheduleStateful("zoom",
CameraState.ENGINE,
new Runnable() {
@Override
@ -622,8 +658,10 @@ public class Camera1Engine extends CameraBaseEngine implements
@Nullable final PointF[] points, final boolean notify) {
final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue;
// EV requests can be high frequency (e.g. linked to touch events), let's trim the oldest.
getOrchestrator().trim("exposure correction", ALLOWED_EV_OPS);
mExposureCorrectionTask = getOrchestrator().scheduleStateful(
"exposure correction (" + EVvalue + ")",
"exposure correction",
CameraState.ENGINE,
new Runnable() {
@Override
@ -715,6 +753,7 @@ public class Camera1Engine extends CameraBaseEngine implements
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) {
@ -745,6 +784,24 @@ public class Camera1Engine extends CameraBaseEngine implements
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) {
@ -795,9 +852,7 @@ public class Camera1Engine extends CameraBaseEngine implements
// Seen this happen in logs.
return;
}
int rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT,
Axis.RELATIVE_TO_SENSOR);
Frame frame = getFrameManager().getFrame(data, System.currentTimeMillis(), rotation);
Frame frame = getFrameManager().getFrame(data, System.currentTimeMillis());
if (frame != null) {
getCallback().dispatchFrame(frame);
}
@ -826,13 +881,18 @@ public class Camera1Engine extends CameraBaseEngine implements
if (maxAF > 0) params.setFocusAreas(transformed.get(maxAF, transform));
if (maxAE > 0) params.setMeteringAreas(transformed.get(maxAE, transform));
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
mCamera.setParameters(params);
try {
mCamera.setParameters(params);
} 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. So we remove the old runnable if still present and post a new one.
getOrchestrator().remove(JOB_FOCUS_END);
getOrchestrator().scheduleDelayed(JOB_FOCUS_END, AUTOFOCUS_END_DELAY_MILLIS,
getOrchestrator().scheduleDelayed(JOB_FOCUS_END, true, AUTOFOCUS_END_DELAY_MILLIS,
new Runnable() {
@Override
public void run() {

@ -60,17 +60,20 @@ import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.frame.ImageFrameManager;
import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.internal.utils.CropHelper;
import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.internal.FpsRangeValidator;
import com.otaliastudios.cameraview.metering.MeteringRegions;
import com.otaliastudios.cameraview.picture.Full2PictureRecorder;
import com.otaliastudios.cameraview.picture.Snapshot2PictureRecorder;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.video.Full2VideoRecorder;
import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
@ -82,7 +85,8 @@ public class Camera2Engine extends CameraBaseEngine implements
ActionHolder {
private static final int FRAME_PROCESSING_FORMAT = ImageFormat.YUV_420_888;
@VisibleForTesting static final long METER_TIMEOUT = 2500;
@VisibleForTesting static final long METER_TIMEOUT = 5000;
private static final long METER_TIMEOUT_SHORT = 2500;
private final CameraManager mManager;
private String mCameraId;
@ -225,6 +229,15 @@ public class Camera2Engine extends CameraBaseEngine implements
}
}
/**
* Can be changed to select something different than {@link CameraDevice#TEMPLATE_PREVIEW}
* for the default repeating request.
* @return the default template for preview
*/
protected int getRepeatingRequestDefaultTemplate() {
return CameraDevice.TEMPLATE_PREVIEW;
}
/**
* Applies the repeating request builder to the preview, assuming we actually have a preview
* running. Can be called after changing parameters to the builder.
@ -416,7 +429,7 @@ public class Camera2Engine extends CameraBaseEngine implements
+ mPictureFormat);
}
mCameraOptions = new Camera2Options(mManager, mCameraId, flip, format);
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW);
createRepeatingRequestBuilder(getRepeatingRequestDefaultTemplate());
} catch (CameraAccessException e) {
task.trySetException(createCameraException(e));
return;
@ -466,7 +479,7 @@ public class Camera2Engine extends CameraBaseEngine implements
// Compute sizes.
// TODO preview stream should never be bigger than 1920x1080 as per
// CameraDevice.createCaptureSession. This should be probably be applied
// CameraDevice.createCaptureSession. This should probably be applied
// before all the other external selectors, to treat it as a hard limit.
// OR: pass an int into these functions to be able to take smaller dims
// when session configuration fails
@ -486,6 +499,7 @@ public class Camera2Engine extends CameraBaseEngine implements
if (outputClass == SurfaceHolder.class) {
try {
// This must be called from the UI thread...
LOG.i("onStartBind:", "Waiting on UI thread...");
Tasks.await(Tasks.call(new Callable<Void>() {
@Override
public Void call() {
@ -576,9 +590,15 @@ public class Camera2Engine extends CameraBaseEngine implements
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
// This SHOULD be a library error so we throw a RuntimeException.
String message = LOG.e("onConfigureFailed! Session", session);
throw new RuntimeException(message);
Throwable cause = new RuntimeException(message);
if (!task.getTask().isComplete()) {
task.trySetException(new CameraException(cause,
CameraException.REASON_FAILED_TO_START_PREVIEW));
} else {
// Like onStartEngine.onError
throw new CameraException(CameraException.REASON_DISCONNECTED);
}
}
@Override
@ -607,7 +627,7 @@ public class Camera2Engine extends CameraBaseEngine implements
mPreview.setStreamSize(previewSizeForView.getWidth(), previewSizeForView.getHeight());
mPreview.setDrawRotation(getAngles().offset(Reference.BASE, Reference.VIEW, Axis.ABSOLUTE));
if (hasFrameProcessors()) {
getFrameManager().setUp(mFrameProcessingFormat, mFrameProcessingSize);
getFrameManager().setUp(mFrameProcessingFormat, mFrameProcessingSize, getAngles());
}
LOG.i("onStartPreview:", "Starting preview.");
@ -769,7 +789,7 @@ public class Camera2Engine extends CameraBaseEngine implements
boolean doMetering) {
if (doMetering) {
LOG.i("onTakePictureSnapshot:", "doMetering is true. Delaying.");
Action action = Actions.timeout(METER_TIMEOUT, createMeterAction(null));
Action action = Actions.timeout(METER_TIMEOUT_SHORT, createMeterAction(null));
action.addCallback(new CompletionCallback() {
@Override
protected void onActionCompleted(@NonNull Action action) {
@ -782,17 +802,16 @@ public class Camera2Engine extends CameraBaseEngine implements
action.start(this);
} else {
LOG.i("onTakePictureSnapshot:", "doMetering is false. Performing.");
if (!(mPreview instanceof GlCameraPreview)) {
if (!(mPreview instanceof RendererCameraPreview)) {
throw new RuntimeException("takePictureSnapshot with Camera2 is only " +
"supported with Preview.GL_SURFACE");
}
// stub.size is not the real size: it will be cropped to the given ratio
// stub.rotation will be set to 0 - we rotate the texture instead.
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT,
Axis.RELATIVE_TO_SENSOR);
stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
mPictureRecorder = new Snapshot2PictureRecorder(stub, this,
(GlCameraPreview) mPreview, outputRatio);
(RendererCameraPreview) mPreview, outputRatio);
mPictureRecorder.take();
}
}
@ -802,7 +821,7 @@ public class Camera2Engine extends CameraBaseEngine implements
protected void onTakePicture(@NonNull final PictureResult.Stub stub, boolean doMetering) {
if (doMetering) {
LOG.i("onTakePicture:", "doMetering is true. Delaying.");
Action action = Actions.timeout(METER_TIMEOUT, createMeterAction(null));
Action action = Actions.timeout(METER_TIMEOUT_SHORT, createMeterAction(null));
action.addCallback(new CompletionCallback() {
@Override
protected void onActionCompleted(@NonNull Action action) {
@ -907,10 +926,10 @@ public class Camera2Engine extends CameraBaseEngine implements
@Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub,
@NonNull AspectRatio outputRatio) {
if (!(mPreview instanceof GlCameraPreview)) {
if (!(mPreview instanceof RendererCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE.");
}
GlCameraPreview glPreview = (GlCameraPreview) mPreview;
RendererCameraPreview glPreview = (RendererCameraPreview) mPreview;
Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null.");
@ -918,24 +937,10 @@ public class Camera2Engine extends CameraBaseEngine implements
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height());
stub.size = outputSize;
// Vertical: 0 (270-0-0)
// Left (unlocked): 270 (270-90-270)
// Right (unlocked): 90 (270-270-90)
// Upside down (unlocked): 180 (270-180-180)
// Left (locked): 270 (270-0-270)
// Right (locked): 90 (270-0-90)
// Upside down (locked): 180 (270-0-180)
// Unlike Camera1, the correct formula seems to be deviceOrientation,
// which means offset(Reference.BASE, Reference.OUTPUT, Axis.ABSOLUTE).
stub.rotation = getAngles().offset(Reference.BASE, 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);
// Start.
// The overlay rotation should alway be VIEW-OUTPUT, just liek Camera1Engine.
int overlayRotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getOverlay(),
overlayRotation);
mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getOverlay());
mVideoRecorder.start(stub);
}
@ -989,9 +994,9 @@ public class Camera2Engine extends CameraBaseEngine implements
@EngineThread
private void maybeRestorePreviewTemplateAfterVideo() {
int template = (int) mRepeatingRequestBuilder.build().getTag();
if (template != CameraDevice.TEMPLATE_PREVIEW) {
if (template != getRepeatingRequestDefaultTemplate()) {
try {
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW);
createRepeatingRequestBuilder(getRepeatingRequestDefaultTemplate());
addRepeatingRequestBuilderSurfaces();
applyRepeatingRequestBuilder();
} catch (CameraAccessException e) {
@ -1258,8 +1263,10 @@ public class Camera2Engine extends CameraBaseEngine implements
public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) {
final float old = mZoomValue;
mZoomValue = zoom;
// Zoom requests can be high frequency (e.g. linked to touch events), let's trim the oldest.
getOrchestrator().trim("zoom", ALLOWED_ZOOM_OPS);
mZoomTask = getOrchestrator().scheduleStateful(
"zoom (" + zoom + ")",
"zoom",
CameraState.ENGINE,
new Runnable() {
@Override
@ -1314,8 +1321,10 @@ public class Camera2Engine extends CameraBaseEngine implements
final boolean notify) {
final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue;
// EV requests can be high frequency (e.g. linked to touch events), let's trim the oldest.
getOrchestrator().trim("exposure correction", ALLOWED_EV_OPS);
mExposureCorrectionTask = getOrchestrator().scheduleStateful(
"exposure correction (" + EVvalue + ")",
"exposure correction",
CameraState.ENGINE,
new Runnable() {
@Override
@ -1373,13 +1382,13 @@ public class Camera2Engine extends CameraBaseEngine implements
protected boolean applyPreviewFrameRate(@NonNull CaptureRequest.Builder builder,
float oldPreviewFrameRate) {
//noinspection unchecked
Range<Integer>[] fallback = new Range[]{};
Range<Integer>[] fpsRanges = readCharacteristic(
CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES,
fallback);
new Range[]{});
sortFrameRateRanges(fpsRanges);
if (mPreviewFrameRate == 0F) {
// 0F is a special value. Fallback to a reasonable default.
for (Range<Integer> fpsRange : fpsRanges) {
for (Range<Integer> fpsRange : filterFrameRateRanges(fpsRanges)) {
if (fpsRange.contains(30) || fpsRange.contains(24)) {
builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
return true;
@ -1391,7 +1400,7 @@ public class Camera2Engine extends CameraBaseEngine implements
mCameraOptions.getPreviewFrameRateMaxValue());
mPreviewFrameRate = Math.max(mPreviewFrameRate,
mCameraOptions.getPreviewFrameRateMinValue());
for (Range<Integer> fpsRange : fpsRanges) {
for (Range<Integer> fpsRange : filterFrameRateRanges(fpsRanges)) {
if (fpsRange.contains(Math.round(mPreviewFrameRate))) {
builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
return true;
@ -1402,6 +1411,35 @@ public class Camera2Engine extends CameraBaseEngine implements
return false;
}
private void sortFrameRateRanges(@NonNull Range<Integer>[] fpsRanges) {
final boolean ascending = getPreviewFrameRateExact() && mPreviewFrameRate != 0;
Arrays.sort(fpsRanges, new Comparator<Range<Integer>>() {
@Override
public int compare(Range<Integer> range1, Range<Integer> range2) {
if (ascending) {
return (range1.getUpper() - range1.getLower())
- (range2.getUpper() - range2.getLower());
} else {
return (range2.getUpper() - range2.getLower())
- (range1.getUpper() - range1.getLower());
}
}
});
}
@NonNull
protected List<Range<Integer>> filterFrameRateRanges(@NonNull Range<Integer>[] fpsRanges) {
List<Range<Integer>> results = new ArrayList<>();
int min = Math.round(mCameraOptions.getPreviewFrameRateMinValue());
int max = Math.round(mCameraOptions.getPreviewFrameRateMaxValue());
for (Range<Integer> fpsRange : fpsRanges) {
if (!fpsRange.contains(min) && !fpsRange.contains(max)) continue;
if (!FpsRangeValidator.validate(fpsRange)) continue;
results.add(fpsRange);
}
return results;
}
@Override
public void setPictureFormat(final @NonNull PictureFormat pictureFormat) {
if (pictureFormat != mPictureFormat) {
@ -1441,10 +1479,7 @@ public class Camera2Engine extends CameraBaseEngine implements
// After preview, the frame manager is correctly set up
//noinspection unchecked
Frame frame = getFrameManager().getFrame(image,
System.currentTimeMillis(),
getAngles().offset(Reference.SENSOR,
Reference.OUTPUT,
Axis.RELATIVE_TO_SENSOR));
System.currentTimeMillis());
if (frame != null) {
LOG.v("onImageAvailable:", "Image acquired, dispatching.");
getCallback().dispatchFrame(frame);

@ -1,7 +1,5 @@
package com.otaliastudios.cameraview.engine;
import android.graphics.PointF;
import android.graphics.RectF;
import android.location.Location;
import androidx.annotation.CallSuper;
@ -16,6 +14,7 @@ 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;
@ -27,7 +26,6 @@ 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.gesture.Gesture;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.picture.PictureRecorder;
import com.otaliastudios.cameraview.preview.CameraPreview;
@ -38,6 +36,7 @@ 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;
@ -48,6 +47,9 @@ import java.util.List;
*/
public abstract class CameraBaseEngine extends CameraEngine {
protected final static int ALLOWED_ZOOM_OPS = 20;
protected final static int ALLOWED_EV_OPS = 20;
@SuppressWarnings("WeakerAccess") protected CameraPreview mPreview;
@SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions;
@SuppressWarnings("WeakerAccess") protected PictureRecorder mPictureRecorder;
@ -60,6 +62,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
@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;
@ -69,6 +72,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
@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();
@ -241,6 +245,17 @@ public abstract class CameraBaseEngine extends CameraEngine {
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;
@ -440,6 +455,16 @@ public abstract class CameraBaseEngine extends CameraEngine {
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;
@ -528,16 +553,16 @@ public abstract class CameraBaseEngine extends CameraEngine {
@Override
public void onPictureShutter(boolean didPlaySound) {
getCallback().onShutter(!didPlaySound);
getCallback().dispatchOnPictureShutter(!didPlaySound);
}
@Override
public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) {
mPictureRecorder = null;
if (result != null) {
if (result != null && result.data != null) {
getCallback().dispatchOnPictureTaken(result);
} else {
LOG.e("onPictureResult", "result is null: something went wrong.", error);
LOG.e("onPictureResult", "result or data is null: something went wrong.", error);
getCallback().dispatchError(new CameraException(error,
CameraException.REASON_PICTURE_FAILED));
}
@ -549,7 +574,9 @@ public abstract class CameraBaseEngine extends CameraEngine {
}
@Override
public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) {
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() {
@ -558,9 +585,16 @@ public abstract class CameraBaseEngine extends CameraEngine {
if (mMode == Mode.PICTURE) {
throw new IllegalStateException("Can't record video while in PICTURE mode");
}
stub.file = file;
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;
@ -588,6 +622,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
stub.file = file;
stub.isSnapshot = true;
stub.videoCodec = mVideoCodec;
stub.audioCodec = mAudioCodec;
stub.location = mLocation;
stub.facing = mFacing;
stub.videoBitRate = mVideoBitRate;

@ -2,7 +2,6 @@ package com.otaliastudios.cameraview.engine;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.RectF;
import android.location.Location;
@ -18,6 +17,7 @@ import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.orchestrator.CameraOrchestrator;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
@ -29,7 +29,7 @@ import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.picture.PictureRecorder;
import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.controls.Audio;
@ -49,6 +49,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.io.FileDescriptor;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -115,7 +116,7 @@ public abstract class CameraEngine implements
void dispatchOnCameraOpened(@NonNull CameraOptions options);
void dispatchOnCameraClosed();
void onCameraPreviewStreamSizeChanged();
void onShutter(boolean shouldPlaySound);
void dispatchOnPictureShutter(boolean shouldPlaySound);
void dispatchOnVideoTaken(@NonNull VideoResult.Stub stub);
void dispatchOnPictureTaken(@NonNull PictureResult.Stub stub);
void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where);
@ -637,6 +638,9 @@ public abstract class CameraEngine implements
public abstract void setAudioBitRate(int audioBitRate);
public abstract int getAudioBitRate();
public abstract void setAudioCodec(@NonNull AudioCodec codec);
@NonNull public abstract AudioCodec getAudioCodec();
public abstract void setSnapshotMaxWidth(int maxWidth);
public abstract int getSnapshotMaxWidth();
@ -689,6 +693,8 @@ public abstract class CameraEngine implements
public abstract void setPictureFormat(@NonNull PictureFormat pictureFormat);
@NonNull public abstract PictureFormat getPictureFormat();
public abstract void setPreviewFrameRateExact(boolean previewFrameRateExact);
public abstract boolean getPreviewFrameRateExact();
public abstract void setPreviewFrameRate(float previewFrameRate);
public abstract float getPreviewFrameRate();
@ -712,7 +718,9 @@ public abstract class CameraEngine implements
public abstract void takePictureSnapshot(final @NonNull PictureResult.Stub stub);
public abstract boolean isTakingVideo();
public abstract void takeVideo(@NonNull VideoResult.Stub stub, @NonNull File file);
public abstract void takeVideo(@NonNull VideoResult.Stub stub,
@Nullable File file,
@Nullable FileDescriptor fileDescriptor);
public abstract void takeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull File file);
public abstract void stopVideo();

@ -35,7 +35,7 @@ public class LogAction extends BaseAction {
" afState: " + afState + " afTriggerState: " + afTriggerState;
if (!log.equals(lastLog)) {
lastLog = log;
LOG.v(log);
LOG.i(log);
}
}

@ -24,6 +24,9 @@ public class ExposureMeter extends BaseMeter {
private static final int STATE_WAITING_PRECAPTURE = 0;
private static final int STATE_WAITING_PRECAPTURE_END = 1;
private boolean mSupportsAreas = false;
private boolean mSupportsTrigger = false;
@SuppressWarnings("WeakerAccess")
public ExposureMeter(@NonNull List<MeteringRectangle> areas, boolean skipIfPossible) {
super(areas, skipIfPossible);
@ -32,9 +35,9 @@ public class ExposureMeter extends BaseMeter {
@Override
protected boolean checkIsSupported(@NonNull ActionHolder holder) {
// In our case, this means checking if we support the AE precapture trigger.
boolean isNotLegacy = readCharacteristic(
boolean isLegacy = readCharacteristic(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1)
!= CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
== CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
Integer aeMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AE_MODE);
boolean isAEOn = aeMode != null &&
(aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON
@ -43,8 +46,13 @@ public class ExposureMeter extends BaseMeter {
|| aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
|| aeMode == 5
/* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */);
boolean result = isNotLegacy && isAEOn;
LOG.i("checkIsSupported:", result);
mSupportsTrigger = !isLegacy;
mSupportsAreas = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE,
0) > 0;
boolean result = isAEOn && (mSupportsTrigger || mSupportsAreas);
LOG.i("checkIsSupported:", result,
"trigger:", mSupportsTrigger,
"areas:", mSupportsAreas);
return result;
}
@ -66,22 +74,26 @@ public class ExposureMeter extends BaseMeter {
protected void onStarted(@NonNull ActionHolder holder, @NonNull List<MeteringRectangle> areas) {
LOG.i("onStarted:", "with areas:", areas);
// Launch the precapture trigger.
holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
// Check the regions.
int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE,
0);
if (!areas.isEmpty() && maxRegions > 0) {
int max = Math.min(maxRegions, areas.size());
if (mSupportsAreas && !areas.isEmpty()) {
int max = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0);
max = Math.min(max, areas.size());
holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_REGIONS,
areas.subList(0, max).toArray(new MeteringRectangle[]{}));
}
if (mSupportsTrigger) {
holder.getBuilder(this).set(
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
}
// Apply
holder.applyBuilder(this);
setState(STATE_WAITING_PRECAPTURE);
if (mSupportsTrigger) {
setState(STATE_WAITING_PRECAPTURE);
} else {
setState(STATE_WAITING_PRECAPTURE_END);
}
}
@Override

@ -13,7 +13,7 @@ import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper;
import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles;
import com.otaliastudios.cameraview.internal.CamcorderProfiles;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;

@ -21,7 +21,7 @@ import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.engine.mappers.Camera2Mapper;
import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles;
import com.otaliastudios.cameraview.internal.CamcorderProfiles;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;

@ -1,5 +1,6 @@
package com.otaliastudios.cameraview.engine.orchestrator;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -8,13 +9,14 @@ import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
@ -40,36 +42,43 @@ public class CameraOrchestrator {
void handleJobException(@NonNull String job, @NonNull Exception exception);
}
protected static class Token {
protected static class Job<T> {
public final String name;
public final Task<?> task;
public final TaskCompletionSource<T> source = new TaskCompletionSource<>();
public final Callable<Task<T>> scheduler;
public final boolean dispatchExceptions;
public final long startTime;
private Token(@NonNull String name, @NonNull Task<?> task) {
private Job(@NonNull String name, @NonNull Callable<Task<T>> scheduler, boolean dispatchExceptions, long startTime) {
this.name = name;
this.task = task;
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof Token && ((Token) obj).name.equals(name);
this.scheduler = scheduler;
this.dispatchExceptions = dispatchExceptions;
this.startTime = startTime;
}
}
protected final Callback mCallback;
protected final ArrayDeque<Token> mJobs = new ArrayDeque<>();
protected final Object mLock = new Object();
private final Map<String, Runnable> mDelayedJobs = new HashMap<>();
protected final ArrayDeque<Job<?>> mJobs = new ArrayDeque<>();
protected boolean mJobRunning = false;
protected final Object mJobsLock = new Object();
public CameraOrchestrator(@NonNull Callback callback) {
mCallback = callback;
ensureToken();
}
@NonNull
public Task<Void> schedule(@NonNull String name,
boolean dispatchExceptions,
@NonNull final Runnable job) {
return schedule(name, dispatchExceptions, new Callable<Task<Void>>() {
@NonNull Runnable job) {
return scheduleDelayed(name, dispatchExceptions, 0L, job);
}
@NonNull
public Task<Void> scheduleDelayed(@NonNull String name,
boolean dispatchExceptions,
long minDelay,
@NonNull final Runnable job) {
return scheduleInternal(name, dispatchExceptions, minDelay, new Callable<Task<Void>>() {
@Override
public Task<Void> call() {
job.run();
@ -78,98 +87,147 @@ public class CameraOrchestrator {
});
}
@SuppressWarnings("unchecked")
@NonNull
public <T> Task<T> schedule(@NonNull final String name,
final boolean dispatchExceptions,
@NonNull final Callable<Task<T>> job) {
public <T> Task<T> schedule(@NonNull String name,
boolean dispatchExceptions,
@NonNull Callable<Task<T>> scheduler) {
return scheduleInternal(name, dispatchExceptions, 0L, scheduler);
}
@NonNull
private <T> Task<T> scheduleInternal(@NonNull String name,
boolean dispatchExceptions,
long minDelay,
@NonNull Callable<Task<T>> scheduler) {
LOG.i(name.toUpperCase(), "- Scheduling.");
final TaskCompletionSource<T> source = new TaskCompletionSource<>();
final WorkerHandler handler = mCallback.getJobWorker(name);
synchronized (mLock) {
applyCompletionListener(mJobs.getLast().task, handler,
new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
synchronized (mLock) {
mJobs.removeFirst();
ensureToken();
}
try {
LOG.i(name.toUpperCase(), "- Executing.");
Task<T> inner = job.call();
applyCompletionListener(inner, handler, new OnCompleteListener<T>() {
@Override
public void onComplete(@NonNull Task<T> task) {
Exception e = task.getException();
if (e != null) {
LOG.w(name.toUpperCase(), "- Finished with ERROR.", e);
if (dispatchExceptions) {
mCallback.handleJobException(name, e);
}
source.trySetException(e);
} else if (task.isCanceled()) {
LOG.i(name.toUpperCase(), "- Finished because ABORTED.");
source.trySetException(new CancellationException());
} else {
LOG.i(name.toUpperCase(), "- Finished.");
source.trySetResult(task.getResult());
}
Job<T> job = new Job<>(name, scheduler, dispatchExceptions,
System.currentTimeMillis() + minDelay);
synchronized (mJobsLock) {
mJobs.addLast(job);
sync(minDelay);
}
return job.source.getTask();
}
@GuardedBy("mJobsLock")
private void sync(long after) {
// Jumping on the message handler even if after = 0L should avoid StackOverflow errors.
mCallback.getJobWorker("_sync").post(after, new Runnable() {
@SuppressWarnings("StatementWithEmptyBody")
@Override
public void run() {
Job<?> job = null;
synchronized (mJobsLock) {
if (mJobRunning) {
// Do nothing, job will be picked in executed().
} else {
long now = System.currentTimeMillis();
for (Job<?> candidate : mJobs) {
if (candidate.startTime <= now) {
job = candidate;
break;
}
});
} catch (Exception e) {
LOG.i(name.toUpperCase(), "- Finished.", e);
if (dispatchExceptions) mCallback.handleJobException(name, e);
source.trySetException(e);
}
if (job != null) {
mJobRunning = true;
}
}
}
});
mJobs.addLast(new Token(name, source.getTask()));
}
return source.getTask();
// This must be out of mJobsLock! See comments in execute().
if (job != null) execute(job);
}
});
}
public void scheduleDelayed(@NonNull final String name,
long minDelay,
@NonNull final Runnable runnable) {
Runnable wrapper = new Runnable() {
// Since we use WorkerHandler.run(), the job can end up being executed on the current thread.
// For this reason, it's important that this method is never guarded by mJobsLock! Because
// all threads can be waiting on that, even the UI thread e.g. through scheduleInternal.
private <T> void execute(@NonNull final Job<T> job) {
final WorkerHandler worker = mCallback.getJobWorker(job.name);
worker.run(new Runnable() {
@Override
public void run() {
schedule(name, true, runnable);
synchronized (mLock) {
if (mDelayedJobs.containsValue(this)) {
mDelayedJobs.remove(name);
try {
LOG.i(job.name.toUpperCase(), "- Executing.");
Task<T> task = job.scheduler.call();
onComplete(task, worker, new OnCompleteListener<T>() {
@Override
public void onComplete(@NonNull Task<T> task) {
Exception e = task.getException();
if (e != null) {
LOG.w(job.name.toUpperCase(), "- Finished with ERROR.", e);
if (job.dispatchExceptions) {
mCallback.handleJobException(job.name, e);
}
job.source.trySetException(e);
} else if (task.isCanceled()) {
LOG.i(job.name.toUpperCase(), "- Finished because ABORTED.");
job.source.trySetException(new CancellationException());
} else {
LOG.i(job.name.toUpperCase(), "- Finished.");
job.source.trySetResult(task.getResult());
}
synchronized (mJobsLock) {
executed(job);
}
}
});
} catch (Exception e) {
LOG.i(job.name.toUpperCase(), "- Finished with ERROR.", e);
if (job.dispatchExceptions) {
mCallback.handleJobException(job.name, e);
}
job.source.trySetException(e);
synchronized (mJobsLock) {
executed(job);
}
}
}
};
synchronized (mLock) {
mDelayedJobs.put(name, wrapper);
mCallback.getJobWorker(name).post(minDelay, wrapper);
});
}
@GuardedBy("mJobsLock")
private <T> void executed(Job<T> job) {
if (!mJobRunning) {
throw new IllegalStateException("mJobRunning was not true after completing job=" + job.name);
}
mJobRunning = false;
mJobs.remove(job);
sync(0L);
}
public void remove(@NonNull String name) {
synchronized (mLock) {
if (mDelayedJobs.get(name) != null) {
//noinspection ConstantConditions
mCallback.getJobWorker(name).remove(mDelayedJobs.get(name));
mDelayedJobs.remove(name);
trim(name, 0);
}
public void trim(@NonNull String name, int allowed) {
synchronized (mJobsLock) {
List<Job<?>> scheduled = new ArrayList<>();
for (Job<?> job : mJobs) {
if (job.name.equals(name)) {
scheduled.add(job);
}
}
LOG.v("trim: name=", name, "scheduled=", scheduled.size(), "allowed=", allowed);
int existing = Math.max(scheduled.size() - allowed, 0);
if (existing > 0) {
// To remove the oldest ones first, we must reverse the list.
// Note that we will potentially remove a job that is being executed: we don't
// have a mechanism to cancel the ongoing execution, but it shouldn't be a problem.
Collections.reverse(scheduled);
scheduled = scheduled.subList(0, existing);
for (Job<?> job : scheduled) {
mJobs.remove(job);
}
}
Token token = new Token(name, Tasks.forResult(null));
//noinspection StatementWithEmptyBody
while (mJobs.remove(token)) { /* do nothing */ }
ensureToken();
}
}
public void reset() {
synchronized (mLock) {
List<String> all = new ArrayList<>();
//noinspection CollectionAddAllCanBeReplacedWithConstructor
all.addAll(mDelayedJobs.keySet());
for (Token token : mJobs) {
all.add(token.name);
synchronized (mJobsLock) {
Set<String> all = new HashSet<>();
for (Job<?> job : mJobs) {
all.add(job.name);
}
for (String job : all) {
remove(job);
@ -177,17 +235,9 @@ public class CameraOrchestrator {
}
}
private void ensureToken() {
synchronized (mLock) {
if (mJobs.isEmpty()) {
mJobs.add(new Token("BASE", Tasks.forResult(null)));
}
}
}
private static <T> void applyCompletionListener(@NonNull final Task<T> task,
@NonNull WorkerHandler handler,
@NonNull final OnCompleteListener<T> listener) {
private static <T> void onComplete(@NonNull final Task<T> task,
@NonNull WorkerHandler handler,
@NonNull final OnCompleteListener<T> listener) {
if (task.isComplete()) {
handler.run(new Runnable() {
@Override

@ -35,10 +35,10 @@ public class CameraStateOrchestrator extends CameraOrchestrator {
}
public boolean hasPendingStateChange() {
synchronized (mLock) {
for (Token token : mJobs) {
if ((token.name.contains(" >> ") || token.name.contains(" << "))
&& !token.task.isComplete()) {
synchronized (mJobsLock) {
for (Job<?> job : mJobs) {
if ((job.name.contains(" >> ") || job.name.contains(" << "))
&& !job.source.getTask().isComplete()) {
return true;
}
}
@ -107,7 +107,7 @@ public class CameraStateOrchestrator extends CameraOrchestrator {
@NonNull final CameraState atLeast,
long delay,
@NonNull final Runnable job) {
scheduleDelayed(name, delay, new Runnable() {
scheduleDelayed(name, true, delay, new Runnable() {
@Override
public void run() {
if (getCurrentState().isAtLeast(atLeast)) {

@ -1,15 +1,13 @@
package com.otaliastudios.cameraview.filter;
import android.opengl.GLES20;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.cameraview.size.Size;
import java.nio.FloatBuffer;
import com.otaliastudios.opengl.draw.GlDrawable;
import com.otaliastudios.opengl.draw.GlRect;
import com.otaliastudios.opengl.program.GlTextureProgram;
/**
* A base implementation of {@link Filter} that just leaves the fragment shader to subclasses.
@ -90,26 +88,8 @@ public abstract class BaseFilter implements Filter {
+ "}\n";
}
// When the model/view/projection matrix is identity, this will exactly cover the viewport.
private FloatBuffer vertexPosition = GlUtils.floatBuffer(new float[]{
-1.0f, -1.0f, // 0 bottom left
1.0f, -1.0f, // 1 bottom right
-1.0f, 1.0f, // 2 top left
1.0f, 1.0f, // 3 top right
});
private FloatBuffer textureCoordinates = GlUtils.floatBuffer(new float[]{
0.0f, 0.0f, // 0 bottom left
1.0f, 0.0f, // 1 bottom right
0.0f, 1.0f, // 2 top left
1.0f, 1.0f // 3 top right
});
private int vertexModelViewProjectionMatrixLocation = -1;
private int vertexTransformMatrixLocation = -1;
private int vertexPositionLocation = -1;
private int vertexTextureCoordinateLocation = -1;
@VisibleForTesting int programHandle = -1;
@VisibleForTesting GlTextureProgram program = null;
private GlDrawable programDrawable = null;
@VisibleForTesting Size size;
@SuppressWarnings("WeakerAccess")
@ -141,28 +121,23 @@ public abstract class BaseFilter implements Filter {
@Override
public void onCreate(int programHandle) {
this.programHandle = programHandle;
vertexPositionLocation = GLES20.glGetAttribLocation(programHandle, vertexPositionName);
GlUtils.checkLocation(vertexPositionLocation, vertexPositionName);
vertexTextureCoordinateLocation = GLES20.glGetAttribLocation(programHandle,
vertexTextureCoordinateName);
GlUtils.checkLocation(vertexTextureCoordinateLocation, vertexTextureCoordinateName);
vertexModelViewProjectionMatrixLocation = GLES20.glGetUniformLocation(programHandle,
vertexModelViewProjectionMatrixName);
GlUtils.checkLocation(vertexModelViewProjectionMatrixLocation,
vertexModelViewProjectionMatrixName);
vertexTransformMatrixLocation = GLES20.glGetUniformLocation(programHandle,
program = new GlTextureProgram(programHandle,
vertexPositionName,
vertexModelViewProjectionMatrixName,
vertexTextureCoordinateName,
vertexTransformMatrixName);
GlUtils.checkLocation(vertexTransformMatrixLocation, vertexTransformMatrixName);
programDrawable = new GlRect();
}
@Override
public void onDestroy() {
programHandle = -1;
vertexPositionLocation = -1;
vertexTextureCoordinateLocation = -1;
vertexModelViewProjectionMatrixLocation = -1;
vertexTransformMatrixLocation = -1;
// Since we used the handle constructor of GlTextureProgram, calling release here
// will NOT destroy the GL program. This is important because Filters are not supposed
// to have ownership of programs. Creation and deletion happen outside, and deleting twice
// would cause an error.
program.release();
program = null;
programDrawable = null;
}
@NonNull
@ -177,8 +152,8 @@ public abstract class BaseFilter implements Filter {
}
@Override
public void draw(long timestampUs, float[] transformMatrix) {
if (programHandle == -1) {
public void draw(long timestampUs, @NonNull float[] transformMatrix) {
if (program == null) {
LOG.w("Filter.draw() called after destroying the filter. " +
"This can happen rarely because of threading.");
} else {
@ -188,44 +163,19 @@ public abstract class BaseFilter implements Filter {
}
}
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
// Copy the model / view / projection matrix over.
GLES20.glUniformMatrix4fv(vertexModelViewProjectionMatrixLocation, 1,
false, GlUtils.IDENTITY_MATRIX, 0);
GlUtils.checkError("glUniformMatrix4fv");
// Copy the texture transformation matrix over.
GLES20.glUniformMatrix4fv(vertexTransformMatrixLocation, 1,
false, transformMatrix, 0);
GlUtils.checkError("glUniformMatrix4fv");
// Enable the "aPosition" vertex attribute.
// Connect vertexBuffer to "aPosition".
GLES20.glEnableVertexAttribArray(vertexPositionLocation);
GlUtils.checkError("glEnableVertexAttribArray: " + vertexPositionLocation);
GLES20.glVertexAttribPointer(vertexPositionLocation, 2, GLES20.GL_FLOAT,
false, 8, vertexPosition);
GlUtils.checkError("glVertexAttribPointer");
// Enable the "aTextureCoord" vertex attribute.
// Connect texBuffer to "aTextureCoord".
GLES20.glEnableVertexAttribArray(vertexTextureCoordinateLocation);
GlUtils.checkError("glEnableVertexAttribArray");
GLES20.glVertexAttribPointer(vertexTextureCoordinateLocation, 2, GLES20.GL_FLOAT,
false, 8, textureCoordinates);
GlUtils.checkError("glVertexAttribPointer");
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
program.setTextureTransform(transformMatrix);
program.onPreDraw(programDrawable, programDrawable.getModelMatrix());
}
@SuppressWarnings("WeakerAccess")
protected void onDraw(long timestampUs) {
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GlUtils.checkError("glDrawArrays");
protected void onDraw(@SuppressWarnings("unused") long timestampUs) {
program.onDraw(programDrawable);
}
@SuppressWarnings("WeakerAccess")
protected void onPostDraw(long timestampUs) {
GLES20.glDisableVertexAttribArray(vertexPositionLocation);
GLES20.glDisableVertexAttribArray(vertexTextureCoordinateLocation);
protected void onPostDraw(@SuppressWarnings("unused") long timestampUs) {
program.onPostDraw(programDrawable);
}
@NonNull
@ -244,6 +194,7 @@ public abstract class BaseFilter implements Filter {
return copy;
}
@NonNull
protected BaseFilter onCopy() {
try {
return getClass().newInstance();

@ -70,7 +70,7 @@ public interface Filter {
* @param timestampUs timestamp in microseconds
* @param transformMatrix matrix
*/
void draw(long timestampUs, float[] transformMatrix);
void draw(long timestampUs, @NonNull float[] transformMatrix);
/**
* Called anytime the output size changes.

@ -6,8 +6,12 @@ import android.opengl.GLES20;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.opengl.core.Egloo;
import com.otaliastudios.opengl.program.GlProgram;
import com.otaliastudios.opengl.program.GlTextureProgram;
import com.otaliastudios.opengl.texture.GlFramebuffer;
import com.otaliastudios.opengl.texture.GlTexture;
import java.util.ArrayList;
import java.util.Arrays;
@ -42,13 +46,13 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
@VisibleForTesting
static class State {
@VisibleForTesting boolean isCreated = false;
@VisibleForTesting boolean isProgramCreated = false;
@VisibleForTesting boolean isFramebufferCreated = false;
private boolean sizeChanged = false;
@VisibleForTesting Size size = null;
private int programHandle = -1;
private int framebufferId = -1;
private int textureId = -1;
private GlFramebuffer outputFramebuffer = null;
private GlTexture outputTexture = null;
}
@VisibleForTesting final List<Filter> filters = new ArrayList<>();
@ -62,7 +66,6 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
* Creates a new group with the given filters.
* @param filters children
*/
@SuppressWarnings("WeakerAccess")
public MultiFilter(@NonNull Filter... filters) {
this(Arrays.asList(filters));
}
@ -105,115 +108,95 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
// with cleanup. Cleanup must happen on the GL thread so we'd have to wait
// for new rendering call (which might not even happen).
private void maybeCreate(@NonNull Filter filter, boolean isFirst) {
private void maybeCreateProgram(@NonNull Filter filter, boolean isFirst, boolean isLast) {
State state = states.get(filter);
//noinspection ConstantConditions
if (!state.isCreated) {
state.isCreated = true;
String shader = filter.getFragmentShader();
if (!isFirst) {
// The first shader actually reads from a OES texture, but the others
// will read from the 2d framebuffer texture. This is a dirty hack.
shader = shader.replace("samplerExternalOES ", "sampler2D ");
}
state.programHandle = GlUtils.createProgram(filter.getVertexShader(), shader);
filter.onCreate(state.programHandle);
}
if (state.isProgramCreated) return;
state.isProgramCreated = true;
// The first shader actually reads from a OES texture, but the others
// will read from the 2d framebuffer texture. This is a dirty hack.
String fragmentShader = isFirst
? filter.getFragmentShader()
: filter.getFragmentShader().replace("samplerExternalOES ", "sampler2D ");
String vertexShader = filter.getVertexShader();
state.programHandle = GlProgram.create(vertexShader, fragmentShader);
filter.onCreate(state.programHandle);
}
private void maybeDestroy(@NonNull Filter filter) {
private void maybeDestroyProgram(@NonNull Filter filter) {
State state = states.get(filter);
//noinspection ConstantConditions
if (state.isCreated) {
state.isCreated = false;
filter.onDestroy();
GLES20.glDeleteProgram(state.programHandle);
state.programHandle = -1;
}
if (!state.isProgramCreated) return;
state.isProgramCreated = false;
filter.onDestroy();
GLES20.glDeleteProgram(state.programHandle);
state.programHandle = -1;
}
private void maybeCreateFramebuffer(@NonNull Filter filter, boolean isLast) {
if (isLast) return;
private void maybeCreateFramebuffer(@NonNull Filter filter, boolean isFirst, boolean isLast) {
State state = states.get(filter);
if (isLast) {
//noinspection ConstantConditions
state.sizeChanged = false;
return;
}
//noinspection ConstantConditions
if (state.sizeChanged) {
maybeDestroyFramebuffer(filter);
state.sizeChanged = false;
}
if (!state.isFramebufferCreated) {
state.isFramebufferCreated = true;
int[] framebufferArray = new int[1];
int[] textureArray = new int[1];
GLES20.glGenFramebuffers(1, framebufferArray, 0);
GLES20.glGenTextures(1, textureArray, 0);
state.framebufferId = framebufferArray[0];
state.textureId = textureArray[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, state.textureId);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA,
state.size.getWidth(), state.size.getHeight(), 0, GLES20.GL_RGBA,
GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, state.framebufferId);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,
GLES20.GL_COLOR_ATTACHMENT0,
state.outputTexture = new GlTexture(GLES20.GL_TEXTURE0,
GLES20.GL_TEXTURE_2D,
state.textureId,
0);
int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
throw new RuntimeException("Invalid framebuffer generation. Error:" + status);
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
state.size.getWidth(),
state.size.getHeight());
state.outputFramebuffer = new GlFramebuffer();
state.outputFramebuffer.attach(state.outputTexture);
}
}
private void maybeDestroyFramebuffer(@NonNull Filter filter) {
State state = states.get(filter);
//noinspection ConstantConditions
if (state.isFramebufferCreated) {
state.isFramebufferCreated = false;
GLES20.glDeleteFramebuffers(1, new int[]{state.framebufferId}, 0);
state.framebufferId = -1;
GLES20.glDeleteTextures(1, new int[]{state.textureId}, 0);
state.textureId = -1;
}
if (!state.isFramebufferCreated) return;
state.isFramebufferCreated = false;
state.outputFramebuffer.release();
state.outputFramebuffer = null;
state.outputTexture.release();
state.outputTexture = null;
}
// Any thread...
private void maybeSetSize(@NonNull Filter filter) {
State state = states.get(filter);
//noinspection ConstantConditions
if (size != null && !size.equals(state.size)) {
state.size = size;
state.sizeChanged = true;
filter.setSize(size.getWidth(), size.getHeight());
}
}
@Override
public void onCreate(int programHandle) {
// We'll create children during the draw() op, since some of them
// might have been added after this onCreate() is called.
}
@NonNull
@Override
public String getVertexShader() {
// Whatever, we won't be using this.
return new NoFilter().getVertexShader();
return GlTextureProgram.SIMPLE_VERTEX_SHADER;
}
@NonNull
@Override
public String getFragmentShader() {
// Whatever, we won't be using this.
return new NoFilter().getFragmentShader();
}
@Override
public void onCreate(int programHandle) {
// We'll create children during the draw() op, since some of them
// might have been added after this onCreate() is called.
return GlTextureProgram.SIMPLE_FRAGMENT_SHADER;
}
@Override
@ -221,7 +204,7 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
synchronized (lock) {
for (Filter filter : filters) {
maybeDestroyFramebuffer(filter);
maybeDestroy(filter);
maybeDestroyProgram(filter);
}
}
}
@ -237,16 +220,17 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
}
@Override
public void draw(long timestampUs, float[] transformMatrix) {
public void draw(long timestampUs, @NonNull float[] transformMatrix) {
synchronized (lock) {
for (int i = 0; i < filters.size(); i++) {
boolean isFirst = i == 0;
boolean isLast = i == filters.size() - 1;
Filter filter = filters.get(i);
State state = states.get(filter);
maybeSetSize(filter);
maybeCreate(filter, isFirst);
maybeCreateFramebuffer(filter, isLast);
maybeCreateProgram(filter, isFirst, isLast);
maybeCreateFramebuffer(filter, isFirst, isLast);
//noinspection ConstantConditions
GLES20.glUseProgram(state.programHandle);
@ -255,7 +239,7 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
// Each filter outputs into its own framebuffer object, except the
// last filter, which outputs into the default framebuffer.
if (!isLast) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, state.framebufferId);
state.outputFramebuffer.bind();
GLES20.glClearColor(0, 0, 0, 0);
} else {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
@ -267,16 +251,17 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
if (isFirst) {
filter.draw(timestampUs, transformMatrix);
} else {
filter.draw(timestampUs, GlUtils.IDENTITY_MATRIX);
filter.draw(timestampUs, Egloo.IDENTITY_MATRIX);
}
// Set the input for the next cycle:
// It is the framebuffer texture from this cycle. If this is the last
// filter, reset this value just to cleanup.
if (!isLast) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, state.textureId);
state.outputTexture.bind();
} else {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
}
GLES20.glUseProgram(0);
@ -289,6 +274,9 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
public Filter copy() {
synchronized (lock) {
MultiFilter copy = new MultiFilter();
if (size != null) {
copy.setSize(size.getWidth(), size.getHeight());
}
for (Filter filter : filters) {
copy.addFilter(filter.copy());
}

@ -33,6 +33,7 @@ public final class SimpleFilter extends BaseFilter {
return fragmentShader;
}
@NonNull
@Override
protected BaseFilter onCopy() {
return new SimpleFilter(fragmentShader);

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Attempts to auto-fix the frames based on histogram equalization.
@ -107,7 +107,7 @@ public class AutoFixFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale");
Egloo.checkGlProgramLocation(scaleLocation, "scale");
}
@Override
@ -117,9 +117,9 @@ public class AutoFixFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(scaleLocation, scale);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Adjusts the brightness of the frames.
@ -76,7 +76,7 @@ public class BrightnessFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
brightnessLocation = GLES20.glGetUniformLocation(programHandle, "brightness");
GlUtils.checkLocation(brightnessLocation, "brightness");
Egloo.checkGlProgramLocation(brightnessLocation, "brightness");
}
@Override
@ -86,9 +86,9 @@ public class BrightnessFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(brightnessLocation, brightness);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Adjusts the contrast.
@ -78,7 +78,7 @@ public class ContrastFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
contrastLocation = GLES20.glGetUniformLocation(programHandle, "contrast");
GlUtils.checkLocation(contrastLocation, "contrast");
Egloo.checkGlProgramLocation(contrastLocation, "contrast");
}
@Override
@ -88,9 +88,9 @@ public class ContrastFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(contrastLocation, contrast);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -5,7 +5,7 @@ import android.opengl.GLES20;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
import java.util.Random;
@ -80,9 +80,9 @@ public class DocumentaryFilter extends BaseFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(mScaleLocation, "scale");
Egloo.checkGlProgramLocation(mScaleLocation, "scale");
mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
GlUtils.checkLocation(mMaxDistLocation, "inv_max_dist");
Egloo.checkGlProgramLocation(mMaxDistLocation, "inv_max_dist");
}
@Override
@ -93,7 +93,7 @@ public class DocumentaryFilter extends BaseFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
float[] scale = new float[2];
if (mWidth > mHeight) {
@ -104,12 +104,12 @@ public class DocumentaryFilter extends BaseFilter {
scale[1] = 1f;
}
GLES20.glUniform2fv(mScaleLocation, 1, scale, 0);
GlUtils.checkError("glUniform2fv");
Egloo.checkGlError("glUniform2fv");
float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
float invMaxDist = 1F / maxDist;
GLES20.glUniform1f(mMaxDistLocation, invMaxDist);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.TwoParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Representation of input frames using only two color tones.
@ -131,13 +131,13 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
mFirstColorLocation = GLES20.glGetUniformLocation(programHandle, "first");
GlUtils.checkLocation(mFirstColorLocation, "first");
Egloo.checkGlProgramLocation(mFirstColorLocation, "first");
mSecondColorLocation = GLES20.glGetUniformLocation(programHandle, "second");
GlUtils.checkLocation(mSecondColorLocation, "second");
Egloo.checkGlProgramLocation(mSecondColorLocation, "second");
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
float[] first = new float[]{
Color.red(mFirstColor) / 255f,
@ -150,9 +150,9 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter {
Color.blue(mSecondColor) / 255f
};
GLES20.glUniform3fv(mFirstColorLocation, 1, first, 0);
GlUtils.checkError("glUniform3fv");
Egloo.checkGlError("glUniform3fv");
GLES20.glUniform3fv(mSecondColorLocation, 1, second, 0);
GlUtils.checkError("glUniform3fv");
Egloo.checkGlError("glUniform3fv");
}
@Override

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Applies back-light filling to the frames.
@ -82,9 +82,9 @@ public class FillLightFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
multiplierLocation = GLES20.glGetUniformLocation(programHandle, "mult");
GlUtils.checkLocation(multiplierLocation, "mult");
Egloo.checkGlProgramLocation(multiplierLocation, "mult");
gammaLocation = GLES20.glGetUniformLocation(programHandle, "igamma");
GlUtils.checkLocation(gammaLocation, "igamma");
Egloo.checkGlProgramLocation(gammaLocation, "igamma");
}
@Override
@ -95,17 +95,17 @@ public class FillLightFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
float amount = 1.0f - strength;
float multiplier = 1.0f / (amount * 0.7f + 0.3f);
GLES20.glUniform1f(multiplierLocation, multiplier);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
float fadeGamma = 0.3f;
float faded = fadeGamma + (1.0f - fadeGamma) * multiplier;
float gamma = 1.0f / faded;
GLES20.glUniform1f(gammaLocation, gamma);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Applies gamma correction to the frames.
@ -73,7 +73,7 @@ public class GammaFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
gammaLocation = GLES20.glGetUniformLocation(programHandle, "gamma");
GlUtils.checkLocation(gammaLocation, "gamma");
Egloo.checkGlProgramLocation(gammaLocation, "gamma");
}
@Override
@ -83,9 +83,9 @@ public class GammaFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(gammaLocation, gamma);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
import java.util.Random;
@ -122,11 +122,11 @@ public class GrainFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
strengthLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(strengthLocation, "scale");
Egloo.checkGlProgramLocation(strengthLocation, "scale");
stepXLocation = GLES20.glGetUniformLocation(programHandle, "stepX");
GlUtils.checkLocation(stepXLocation, "stepX");
Egloo.checkGlProgramLocation(stepXLocation, "stepX");
stepYLocation = GLES20.glGetUniformLocation(programHandle, "stepY");
GlUtils.checkLocation(stepYLocation, "stepY");
Egloo.checkGlProgramLocation(stepYLocation, "stepY");
}
@Override
@ -138,13 +138,13 @@ public class GrainFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(strengthLocation, strength);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepXLocation, 0.5f / width);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepYLocation, 0.5f / height);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Applies a hue effect on the input frames.
@ -86,7 +86,7 @@ public class HueFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
hueLocation = GLES20.glGetUniformLocation(programHandle, "hue");
GlUtils.checkLocation(hueLocation, "hue");
Egloo.checkGlProgramLocation(hueLocation, "hue");
}
@Override
@ -96,11 +96,11 @@ public class HueFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
// map it on 360 degree circle
float shaderHue = ((hue - 45) / 45f + 0.5f) * -1;
GLES20.glUniform1f(hueLocation, shaderHue);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -5,7 +5,7 @@ import android.opengl.GLES20;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
import java.util.Random;
@ -122,13 +122,13 @@ public class LomoishFilter extends BaseFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale");
Egloo.checkGlProgramLocation(scaleLocation, "scale");
maxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
GlUtils.checkLocation(maxDistLocation, "inv_max_dist");
Egloo.checkGlProgramLocation(maxDistLocation, "inv_max_dist");
stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX");
GlUtils.checkLocation(stepSizeXLocation, "stepsizeX");
Egloo.checkGlProgramLocation(stepSizeXLocation, "stepsizeX");
stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY");
GlUtils.checkLocation(stepSizeYLocation, "stepsizeY");
Egloo.checkGlProgramLocation(stepSizeYLocation, "stepsizeY");
}
@Override
@ -141,7 +141,7 @@ public class LomoishFilter extends BaseFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
float[] scale = new float[2];
if (width > height) {
@ -153,12 +153,12 @@ public class LomoishFilter extends BaseFilter {
}
float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
GLES20.glUniform2fv(scaleLocation, 1, scale, 0);
GlUtils.checkError("glUniform2fv");
Egloo.checkGlError("glUniform2fv");
GLES20.glUniform1f(maxDistLocation, 1.0F / maxDist);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepSizeXLocation, 1.0F / width);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepSizeYLocation, 1.0F / height);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Adjusts color saturation.
@ -93,9 +93,9 @@ public class SaturationFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale");
Egloo.checkGlProgramLocation(scaleLocation, "scale");
exponentsLocation = GLES20.glGetUniformLocation(programHandle, "exponents");
GlUtils.checkLocation(exponentsLocation, "exponents");
Egloo.checkGlProgramLocation(exponentsLocation, "exponents");
}
@Override
@ -106,22 +106,22 @@ public class SaturationFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
if (scale > 0.0f) {
GLES20.glUniform1f(scaleLocation, 0F);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
GLES20.glUniform3f(exponentsLocation,
(0.9f * scale) + 1.0f,
(2.1f * scale) + 1.0f,
(2.7f * scale) + 1.0f
);
GlUtils.checkError("glUniform3f");
Egloo.checkGlError("glUniform3f");
} else {
GLES20.glUniform1f(scaleLocation, 1.0F + scale);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
GLES20.glUniform3f(exponentsLocation, 0F, 0F, 0F);
GlUtils.checkError("glUniform3f");
Egloo.checkGlError("glUniform3f");
}
}
}

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Sharpens the input frames.
@ -100,11 +100,11 @@ public class SharpnessFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale");
Egloo.checkGlProgramLocation(scaleLocation, "scale");
stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX");
GlUtils.checkLocation(stepSizeXLocation, "stepsizeX");
Egloo.checkGlProgramLocation(stepSizeXLocation, "stepsizeX");
stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY");
GlUtils.checkLocation(stepSizeYLocation, "stepsizeY");
Egloo.checkGlProgramLocation(stepSizeYLocation, "stepsizeY");
}
@Override
@ -116,13 +116,13 @@ public class SharpnessFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(scaleLocation, scale);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepSizeXLocation, 1.0F / width);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepSizeYLocation, 1.0F / height);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
* Adjusts color temperature.
@ -84,7 +84,7 @@ public class TemperatureFilter extends BaseFilter implements OneParameterFilter
public void onCreate(int programHandle) {
super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale");
Egloo.checkGlProgramLocation(scaleLocation, "scale");
}
@Override
@ -94,9 +94,9 @@ public class TemperatureFilter extends BaseFilter implements OneParameterFilter
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(scaleLocation, scale);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
@ -81,7 +81,7 @@ public class TintFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
tintLocation = GLES20.glGetUniformLocation(programHandle, "tint");
GlUtils.checkLocation(tintLocation, "tint");
Egloo.checkGlProgramLocation(tintLocation, "tint");
}
@Override
@ -91,7 +91,7 @@ public class TintFilter extends BaseFilter implements OneParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
float[] channels = new float[]{
Color.red(tint) / 255f,
@ -99,6 +99,6 @@ public class TintFilter extends BaseFilter implements OneParameterFilter {
Color.blue(tint) / 255f
};
GLES20.glUniform3fv(tintLocation, 1, channels, 0);
GlUtils.checkError("glUniform3fv");
Egloo.checkGlError("glUniform3fv");
}
}

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.TwoParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.opengl.core.Egloo;
/**
@ -126,13 +126,13 @@ public class VignetteFilter extends BaseFilter implements TwoParameterFilter {
public void onCreate(int programHandle) {
super.onCreate(programHandle);
mRangeLocation = GLES20.glGetUniformLocation(programHandle, "range");
GlUtils.checkLocation(mRangeLocation, "range");
Egloo.checkGlProgramLocation(mRangeLocation, "range");
mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
GlUtils.checkLocation(mMaxDistLocation, "inv_max_dist");
Egloo.checkGlProgramLocation(mMaxDistLocation, "inv_max_dist");
mShadeLocation = GLES20.glGetUniformLocation(programHandle, "shade");
GlUtils.checkLocation(mShadeLocation, "shade");
Egloo.checkGlProgramLocation(mShadeLocation, "shade");
mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(mScaleLocation, "scale");
Egloo.checkGlProgramLocation(mScaleLocation, "scale");
}
@Override
@ -145,7 +145,7 @@ public class VignetteFilter extends BaseFilter implements TwoParameterFilter {
}
@Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix);
float[] scale = new float[2];
if (mWidth > mHeight) {
@ -156,20 +156,20 @@ public class VignetteFilter extends BaseFilter implements TwoParameterFilter {
scale[1] = 1f;
}
GLES20.glUniform2fv(mScaleLocation, 1, scale, 0);
GlUtils.checkError("glUniform2fv");
Egloo.checkGlError("glUniform2fv");
float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
GLES20.glUniform1f(mMaxDistLocation, 1F / maxDist);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(mShadeLocation, mShade);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
// The 'range' is between 1.3 to 0.6. When scale is zero then range is 1.3
// which means no vignette at all because the luminousity difference is
// less than 1/256 and will cause nothing.
float range = (1.30f - (float) Math.sqrt(mScale) * 0.7f);
GLES20.glUniform1f(mRangeLocation, range);
GlUtils.checkError("glUniform1f");
Egloo.checkGlError("glUniform1f");
}
}

@ -4,6 +4,7 @@ package com.otaliastudios.cameraview.frame;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.size.Size;
import java.util.concurrent.LinkedBlockingQueue;
@ -21,14 +22,14 @@ import java.util.concurrent.LinkedBlockingQueue;
*
* 1. {@link #BUFFER_MODE_DISPATCH}: in this mode, as soon as we have a buffer, it is dispatched to
* the {@link BufferCallback}. The callback should then fill the buffer, and finally call
* {@link FrameManager#getFrame(Object, long, int)} to receive a frame.
* {@link FrameManager#getFrame(Object, long)} to receive a frame.
* This is used for Camera1.
*
* 2. {@link #BUFFER_MODE_ENQUEUE}: in this mode, the manager internally keeps a queue of byte
* buffers, instead of handing them to the callback. The users can ask for buffers through
* {@link #getBuffer()}.
* This buffer can be filled with data and used to get a frame
* {@link FrameManager#getFrame(Object, long, int)}, or, in case it was not filled, returned to
* {@link FrameManager#getFrame(Object, long)}, or, in case it was not filled, returned to
* the queue using {@link #onBufferUnused(byte[])}.
* This is used for Camera2.
*/
@ -60,7 +61,7 @@ public class ByteBufferFrameManager extends FrameManager<byte[]> {
/**
* Construct a new frame manager.
* The construction must be followed by an {@link #setUp(int, Size)} call
* The construction must be followed by an {@link FrameManager#setUp(int, Size, Angles)} call
* as soon as the parameters are known.
*
* @param poolSize the size of the backing pool.
@ -79,8 +80,8 @@ public class ByteBufferFrameManager extends FrameManager<byte[]> {
@Override
public void setUp(int format, @NonNull Size size) {
super.setUp(format, size);
public void setUp(int format, @NonNull Size size, @NonNull Angles angles) {
super.setUp(format, size, angles);
int bytes = getFrameBytes();
for (int i = 0; i < getPoolSize(); i++) {
if (mBufferMode == BUFFER_MODE_DISPATCH) {
@ -97,7 +98,7 @@ public class ByteBufferFrameManager extends FrameManager<byte[]> {
* manager also holds a queue of the byte buffers.
*
* If not null, the buffer returned by this method can be filled and used to get
* a new frame through {@link FrameManager#getFrame(Object, long, int)}.
* a new frame through {@link FrameManager#getFrame(Object, long)}.
*
* @return a buffer, or null
*/

@ -22,7 +22,8 @@ public class Frame {
private Object mData = null;
private long mTime = -1;
private long mLastTime = -1;
private int mRotation = 0;
private int mUserRotation = 0;
private int mViewRotation = 0;
private Size mSize = null;
private int mFormat = -1;
@ -31,13 +32,15 @@ public class Frame {
mDataClass = manager.getFrameDataClass();
}
void setContent(@NonNull Object data, long time, int rotation, @NonNull Size size, int format) {
this.mData = data;
this.mTime = time;
this.mLastTime = time;
this.mRotation = rotation;
this.mSize = size;
this.mFormat = format;
void setContent(@NonNull Object data, long time, int userRotation, int viewRotation,
@NonNull Size size, int format) {
mData = data;
mTime = time;
mLastTime = time;
mUserRotation = userRotation;
mViewRotation = viewRotation;
mSize = size;
mFormat = format;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
@ -75,7 +78,7 @@ public class Frame {
Frame other = new Frame(mManager);
//noinspection unchecked
Object data = mManager.cloneFrameData(getData());
other.setContent(data, mTime, mRotation, mSize, mFormat);
other.setContent(data, mTime, mUserRotation, mViewRotation, mSize, mFormat);
return other;
}
@ -88,7 +91,8 @@ public class Frame {
LOG.v("Frame with time", mTime, "is being released.");
Object data = mData;
mData = null;
mRotation = 0;
mUserRotation = 0;
mViewRotation = 0;
mTime = -1;
mSize = null;
mFormat = -1;
@ -133,16 +137,36 @@ public class Frame {
return mTime;
}
/**
* @deprecated use {@link #getRotationToUser()} instead
*/
@Deprecated
public int getRotation() {
return getRotationToUser();
}
/**
* Returns the clock-wise rotation that should be applied on the data
* array, such that the resulting frame matches what the user is seeing
* on screen.
* on screen. Knowing this can help in the processing phase.
*
* @return clock-wise rotation
*/
public int getRotation() {
public int getRotationToUser() {
ensureHasContent();
return mUserRotation;
}
/**
* Returns the clock-wise rotation that should be applied on the data
* array, such that the resulting frame matches the View / Activity orientation.
* Knowing this can help in the drawing / rendering phase.
*
* @return clock-wise rotation
*/
public int getRotationToView() {
ensureHasContent();
return mRotation;
return mViewRotation;
}
/**

@ -4,6 +4,9 @@ package com.otaliastudios.cameraview.frame;
import android.graphics.ImageFormat;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.offset.Angles;
import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull;
@ -16,9 +19,9 @@ import java.util.concurrent.LinkedBlockingQueue;
* The FrameManager keeps a {@link #mPoolSize} integer that defines the number of instances to keep.
*
* Main methods are:
* - {@link #setUp(int, Size)}: to set up with size and allocate buffers
* - {@link #setUp(int, Size, Angles)}: to set up with size and allocate buffers
* - {@link #release()}: to release. After release, a manager can be setUp again.
* - {@link #getFrame(Object, long, int)}: gets a new {@link Frame}.
* - {@link #getFrame(Object, long)}: gets a new {@link Frame}.
*
* For frames to get back to the FrameManager pool, all you have to do
* is call {@link Frame#release()} when done.
@ -34,11 +37,12 @@ public abstract class FrameManager<T> {
private int mFrameFormat = -1;
private final Class<T> mFrameDataClass;
private LinkedBlockingQueue<Frame> mFrameQueue;
private Angles mAngles;
/**
* Construct a new frame manager.
* The construction must be followed by an {@link #setUp(int, Size)} call
* The construction must be followed by an {@link #setUp(int, Size, Angles)} call
* as soon as the parameters are known.
*
* @param poolSize the size of the backing pool.
@ -80,11 +84,11 @@ public abstract class FrameManager<T> {
* the preview size and the image format value are known.
*
* This method can be called again after {@link #release()} has been called.
*
* @param format the image format
* @param format the image format
* @param size the frame size
* @param angles angle object
*/
public void setUp(int format, @NonNull Size size) {
public void setUp(int format, @NonNull Size size, @NonNull Angles angles) {
if (isSetUp()) {
// TODO throw or just reconfigure?
}
@ -96,10 +100,11 @@ public abstract class FrameManager<T> {
for (int i = 0; i < getPoolSize(); i++) {
mFrameQueue.offer(new Frame(this));
}
mAngles = angles;
}
/**
* Returns true after {@link #setUp(int, Size)}
* Returns true after {@link #setUp(int, Size, Angles)}
* but before {@link #release()}.
* Returns false otherwise.
*
@ -111,16 +116,15 @@ public abstract class FrameManager<T> {
/**
* Returns a new Frame for the given data. This must be called
* - after {@link #setUp(int, Size)}, which sets the buffer size
* - after {@link #setUp(int, Size, Angles)}, which sets the buffer size
* - after the T data has been filled
*
* @param data data
* @param time timestamp
* @param rotation rotation
* @return a new frame
*/
@Nullable
public Frame getFrame(@NonNull T data, long time, int rotation) {
public Frame getFrame(@NonNull T data, long time) {
if (!isSetUp()) {
throw new IllegalStateException("Can't call getFrame() after releasing " +
"or before setUp.");
@ -129,7 +133,11 @@ public abstract class FrameManager<T> {
Frame frame = mFrameQueue.poll();
if (frame != null) {
LOG.v("getFrame for time:", time, "RECYCLING.");
frame.setContent(data, time, rotation, mFrameSize, mFrameFormat);
int userRotation = mAngles.offset(Reference.SENSOR, Reference.OUTPUT,
Axis.RELATIVE_TO_SENSOR);
int viewRotation = mAngles.offset(Reference.SENSOR, Reference.VIEW,
Axis.RELATIVE_TO_SENSOR);
frame.setContent(data, time, userRotation, viewRotation, mFrameSize, mFrameFormat);
return frame;
} else {
LOG.i("getFrame for time:", time, "NOT AVAILABLE.");
@ -183,5 +191,6 @@ public abstract class FrameManager<T> {
mFrameBytes = -1;
mFrameSize = null;
mFrameFormat = -1;
mAngles = null;
}
}

@ -43,6 +43,15 @@ public enum GestureAction {
*/
TAKE_PICTURE(2, GestureType.ONE_SHOT),
/**
* When triggered, this action will fire a picture snapshot.
* This action can be mapped to one shot gestures:
*
* - {@link Gesture#TAP}
* - {@link Gesture#LONG_TAP}
*/
TAKE_PICTURE_SNAPSHOT(3, GestureType.ONE_SHOT),
/**
* Zoom control, typically assigned to the pinch gesture.
* This action can be mapped to continuous gestures:
@ -51,7 +60,7 @@ public enum GestureAction {
* - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL}
*/
ZOOM(3, GestureType.CONTINUOUS),
ZOOM(4, GestureType.CONTINUOUS),
/**
* Exposure correction control.
@ -61,7 +70,7 @@ public enum GestureAction {
* - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL}
*/
EXPOSURE_CORRECTION(4, GestureType.CONTINUOUS),
EXPOSURE_CORRECTION(5, GestureType.CONTINUOUS),
/**
* Controls the first parameter of a real-time {@link Filter},
@ -71,7 +80,7 @@ public enum GestureAction {
* - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL}
*/
FILTER_CONTROL_1(5, GestureType.CONTINUOUS),
FILTER_CONTROL_1(6, GestureType.CONTINUOUS),
/**
* Controls the second parameter of a real-time {@link Filter},
@ -81,7 +90,7 @@ public enum GestureAction {
* - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL}
*/
FILTER_CONTROL_2(6, GestureType.CONTINUOUS);
FILTER_CONTROL_2(7, GestureType.CONTINUOUS);
final static GestureAction DEFAULT_PINCH = NONE;
final static GestureAction DEFAULT_TAP = NONE;

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import android.annotation.SuppressLint;
import android.media.CamcorderProfile;

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import android.graphics.Rect;

@ -7,6 +7,7 @@ import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Build;
import android.util.Range;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -196,10 +197,7 @@ public class DeviceEncoders {
public int compare(MediaCodecInfo o1, MediaCodecInfo o2) {
boolean hw1 = isHardwareEncoder(o1.getName());
boolean hw2 = isHardwareEncoder(o2.getName());
if (hw1 && hw2) return 0;
if (hw1) return -1;
if (hw2) return 1;
return 0;
return Boolean.compare(hw2, hw1);
}
});
}
@ -260,6 +258,28 @@ public class DeviceEncoders {
" Range:" + mVideoCapabilities.getSupportedHeights());
}
// We cannot change the aspect ratio, but the max block count might also be the
// issue. Try to find a width that contains a height that would accept our AR.
try {
if (!mVideoCapabilities.getSupportedHeightsFor(width).contains(height)) {
int candidateWidth = width;
int minWidth = mVideoCapabilities.getSupportedWidths().getLower();
int widthAlignment = mVideoCapabilities.getWidthAlignment();
while (candidateWidth >= minWidth) {
// Reduce by 32 and realign just in case, then check if our AR is now
// supported. If it is, restart from scratch to go through the other checks.
candidateWidth -= 32;
while (candidateWidth % widthAlignment != 0) candidateWidth--;
int candidateHeight = (int) Math.round(candidateWidth / aspect);
if (mVideoCapabilities.getSupportedHeightsFor(candidateWidth)
.contains(candidateHeight)) {
LOG.w("getSupportedVideoSize - restarting with smaller size.");
return getSupportedVideoSize(new Size(candidateWidth, candidateHeight));
}
}
}
} catch (IllegalArgumentException ignore) {}
// It's still possible that we're unsupported for other reasons.
if (!mVideoCapabilities.isSizeSupported(width, height)) {
throw new VideoException("Size not supported for unknown reason." +

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import androidx.exifinterface.media.ExifInterface;

@ -0,0 +1,40 @@
package com.otaliastudios.cameraview.internal;
import android.os.Build;
import android.util.Log;
import android.util.Range;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RequiresApi(21)
public class FpsRangeValidator {
private final static CameraLogger LOG = CameraLogger.create("FpsRangeValidator");
private final static Map<String, List<Range<Integer>>> sIssues = new HashMap<>();
static {
sIssues.put("Google Pixel 4", Arrays.asList(new Range<>(15, 60)));
sIssues.put("Google Pixel 4a", Arrays.asList(new Range<>(15, 60)));
sIssues.put("Google Pixel 4 XL", Arrays.asList(new Range<>(15, 60)));
}
public static boolean validate(Range<Integer> range) {
LOG.i("Build.MODEL:", Build.MODEL, "Build.BRAND:", Build.BRAND, "Build.MANUFACTURER:", Build.MANUFACTURER);
String descriptor = Build.MANUFACTURER + " " + Build.MODEL;
List<Range<Integer>> ranges = sIssues.get(descriptor);
if (ranges != null && ranges.contains(range)) {
LOG.i("Dropping range:", range);
return false;
}
return true;
}
}

@ -0,0 +1,97 @@
package com.otaliastudios.cameraview.internal;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.opengl.core.Egloo;
import com.otaliastudios.opengl.program.GlProgram;
import com.otaliastudios.opengl.texture.GlTexture;
public class GlTextureDrawer {
private final static String TAG = GlTextureDrawer.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
private final static int TEXTURE_TARGET = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
private final static int TEXTURE_UNIT = GLES20.GL_TEXTURE0;
private final GlTexture mTexture;
private float[] mTextureTransform = Egloo.IDENTITY_MATRIX.clone();
@NonNull
private Filter mFilter = new NoFilter();
private Filter mPendingFilter = null;
private int mProgramHandle = -1;
@SuppressWarnings("unused")
public GlTextureDrawer() {
this(new GlTexture(TEXTURE_UNIT, TEXTURE_TARGET));
}
@SuppressWarnings("unused")
public GlTextureDrawer(int textureId) {
this(new GlTexture(TEXTURE_UNIT, TEXTURE_TARGET, textureId));
}
@SuppressWarnings("WeakerAccess")
public GlTextureDrawer(@NonNull GlTexture texture) {
mTexture = texture;
}
public void setFilter(@NonNull Filter filter) {
mPendingFilter = filter;
}
@NonNull
public GlTexture getTexture() {
return mTexture;
}
@NonNull
public float[] getTextureTransform() {
return mTextureTransform;
}
public void setTextureTransform(@NonNull float[] textureTransform) {
mTextureTransform = textureTransform;
}
public void draw(final long timestampUs) {
if (mPendingFilter != null) {
release();
mFilter = mPendingFilter;
mPendingFilter = null;
}
if (mProgramHandle == -1) {
mProgramHandle = GlProgram.create(
mFilter.getVertexShader(),
mFilter.getFragmentShader());
mFilter.onCreate(mProgramHandle);
Egloo.checkGlError("program creation");
}
GLES20.glUseProgram(mProgramHandle);
Egloo.checkGlError("glUseProgram(handle)");
mTexture.bind();
mFilter.draw(timestampUs, mTextureTransform);
mTexture.unbind();
GLES20.glUseProgram(0);
Egloo.checkGlError("glUseProgram(0)");
}
public void release() {
if (mProgramHandle == -1) return;
mFilter.onDestroy();
GLES20.glDeleteProgram(mProgramHandle);
mProgramHandle = -1;
}
}

@ -1,96 +0,0 @@
package com.otaliastudios.cameraview.internal;
import android.opengl.GLES20;
import android.opengl.Matrix;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.CameraLogger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
public class GlUtils {
private final static String TAG = GlUtils.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
// Identity matrix for general use.
public static final float[] IDENTITY_MATRIX = new float[16];
static {
Matrix.setIdentityM(IDENTITY_MATRIX, 0);
}
public static void checkError(@NonNull String opName) {
int error = GLES20.glGetError();
if (error != GLES20.GL_NO_ERROR) {
String message = LOG.e("Error during", opName, "glError 0x",
Integer.toHexString(error));
throw new RuntimeException(message);
}
}
public static void checkLocation(int location, @NonNull String name) {
if (location < 0) {
String message = LOG.e("Unable to locate", name, "in program");
throw new RuntimeException(message);
}
}
// Compiles the given shader, returns a handle.
@SuppressWarnings("WeakerAccess")
public static int loadShader(int shaderType, @NonNull String source) {
int shader = GLES20.glCreateShader(shaderType);
checkError("glCreateShader type=" + shaderType);
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compiled = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
LOG.e("Could not compile shader", shaderType, ":",
GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
return shader;
}
// Creates a program with given vertex shader and pixel shader.
public static int createProgram(@NonNull String vertexSource, @NonNull String fragmentSource) {
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) return 0;
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (pixelShader == 0) return 0;
int program = GLES20.glCreateProgram();
checkError("glCreateProgram");
if (program == 0) {
LOG.e("Could not create program");
}
GLES20.glAttachShader(program, vertexShader);
checkError("glAttachShader");
GLES20.glAttachShader(program, pixelShader);
checkError("glAttachShader");
GLES20.glLinkProgram(program);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
LOG.e("Could not link program:", GLES20.glGetProgramInfoLog(program));
GLES20.glDeleteProgram(program);
program = 0;
}
return program;
}
// Allocates a direct float buffer, and populates it with the float array data.
public static FloatBuffer floatBuffer(@NonNull float[] coords) {
// Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it.
ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * 4);
bb.order(ByteOrder.nativeOrder());
FloatBuffer fb = bb.asFloatBuffer();
fb.put(coords);
fb.position(0);
return fb;
}
}

@ -6,7 +6,6 @@ import android.graphics.SurfaceTexture;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.view.Surface;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.preview.RendererThread;
@ -84,7 +83,7 @@ import com.otaliastudios.cameraview.preview.RendererThread;
*
* This makes no sense, since overlaySurfaceTexture.updateTexImage() is setting it to
* overlayTextureId anyway, but it fixes the issue. Specifically, after any draw operation with
* {@link EglViewport}, the bound texture is reset to 0 so this must be undone here. We offer:
* {@link GlTextureDrawer}, the bound texture is reset to 0 so this must be undone here. We offer:
*
* - {@link #beforeOverlayUpdateTexImage()} to be called before the
* {@link SurfaceTexture#updateTexImage()} call
@ -92,7 +91,7 @@ import com.otaliastudios.cameraview.preview.RendererThread;
*
* Since updating and rendering can happen on different threads with a shared EGL context,
* in case they do, the {@link #beforeOverlayUpdateTexImage()}, the actual updateTexImage() and
* finally the {@link EglViewport} drawing operations should be synchronized with a lock.
* finally the {@link GlTextureDrawer} drawing operations should be synchronized with a lock.
*
* REFERENCES
* https://github.com/natario1/CameraView/issues/514

@ -1,12 +1,17 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.hardware.SensorManager;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.Surface;
@ -16,6 +21,36 @@ import android.view.WindowManager;
* Helps with keeping track of both device orientation (which changes when device is rotated)
* and the display offset (which depends on the activity orientation wrt the device default
* orientation).
*
* Note: any change in the display offset should restart the camera engine, because it reads
* from the angles container at startup and computes size based on that. This is tricky because
* activity behavior can differ:
*
* - if activity is locked to some orientation, {@link #mDisplayOffset} won't change, and
* {@link View#onConfigurationChanged(Configuration)} won't be called.
* The library will work fine.
*
* - if the activity is unlocked and does NOT handle orientation changes with android:configChanges,
* the actual behavior differs depending on the rotation.
* - the configuration callback is never called, of course.
* - for 90°/-90° rotations, the activity is recreated. Sometime you get {@link #mDisplayOffset}
* callback before destruction, sometimes you don't - in any case it's going to recreate.
* - for 180°/-180°, the activity is NOT recreated! But we can rely on {@link #mDisplayOffset}
* changing with a 180 delta and restart the engine.
*
* - lastly, if the activity is unlocked and DOES handle orientation changes with android:configChanges,
* as it will often be the case in a modern Compose app,
* - you always get the {@link #mDisplayOffset} callback
* - for 90°/-90° rotations, the view also gets the configuration changed callback.
* - for 180°/-180°, the view won't get it because configuration only cares about portrait vs. landscape.
*
* In practice, since we don't control the activity and we can't easily inspect the configChanges
* flags at runtime, a good solution is to always restart when the display offset changes. We might
* do useless restarts in one rare scenario (unlocked, no android:configChanges, 90° rotation,
* display offset callback received before destruction) but that's acceptable.
*
* Tried to avoid that by looking at {@link Activity#isChangingConfigurations()}, but it's always
* false by the time the display offset callback is invoked.
*/
public class OrientationHelper {
@ -24,9 +59,10 @@ public class OrientationHelper {
*/
public interface Callback {
void onDeviceOrientationChanged(int deviceOrientation);
void onDisplayOffsetChanged(int displayOffset, boolean willRecreate);
void onDisplayOffsetChanged();
}
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Context mContext;
private final Callback mCallback;
@ -38,6 +74,8 @@ public class OrientationHelper {
final DisplayManager.DisplayListener mDisplayOffsetListener;
private int mDisplayOffset = -1;
private boolean mEnabled;
/**
* Creates a new orientation helper.
* @param context a valid context
@ -82,9 +120,7 @@ public class OrientationHelper {
int newDisplayOffset = findDisplayOffset();
if (newDisplayOffset != oldDisplayOffset) {
mDisplayOffset = newDisplayOffset;
// With 180 degrees flips, the activity is not recreated.
boolean willRecreate = Math.abs(newDisplayOffset - oldDisplayOffset) != 180;
mCallback.onDisplayOffsetChanged(newDisplayOffset, willRecreate);
mCallback.onDisplayOffsetChanged();
}
}
};
@ -97,11 +133,14 @@ public class OrientationHelper {
* Enables this listener.
*/
public void enable() {
if (mEnabled) return;
mEnabled = true;
mDisplayOffset = findDisplayOffset();
if (Build.VERSION.SDK_INT >= 17) {
DisplayManager manager = (DisplayManager)
mContext.getSystemService(Context.DISPLAY_SERVICE);
manager.registerDisplayListener(mDisplayOffsetListener, null);
// Without the handler, this can crash if called from a thread without a looper
manager.registerDisplayListener(mDisplayOffsetListener, mHandler);
}
mDeviceOrientationListener.enable();
}
@ -110,6 +149,8 @@ public class OrientationHelper {
* Disables this listener.
*/
public void disable() {
if (!mEnabled) return;
mEnabled = false;
mDeviceOrientationListener.disable();
if (Build.VERSION.SDK_INT >= 17) {
DisplayManager manager = (DisplayManager)

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import com.otaliastudios.cameraview.CameraLogger;

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import com.otaliastudios.cameraview.size.Size;

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils;
package com.otaliastudios.cameraview.internal;
import android.os.Handler;
import android.os.HandlerThread;

@ -1,244 +0,0 @@
/*
* Copyright 2013 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.otaliastudios.cameraview.internal.egl;
import android.graphics.Bitmap;
import android.opengl.EGL14;
import android.opengl.EGLSurface;
import android.opengl.GLES20;
import android.os.Build;
import androidx.annotation.RequiresApi;
import android.util.Log;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.GlUtils;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Common base class for EGL surfaces.
* <p>
* There can be multiple surfaces associated with a single context.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class EglBaseSurface {
protected static final String TAG = EglBaseSurface.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
// EglCore object we're associated with. It may be associated with multiple surfaces.
protected EglCore mEglCore;
private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;
private int mWidth = -1;
private int mHeight = -1;
public EglBaseSurface(EglCore eglCore) {
mEglCore = eglCore;
}
/**
* Creates a window surface.
* <p>
* @param surface May be a Surface or SurfaceTexture.
*/
public void createWindowSurface(Object surface) {
if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
throw new IllegalStateException("surface already created");
}
mEGLSurface = mEglCore.createWindowSurface(surface);
// Don't cache width/height here, because the size of the underlying surface can change
// out from under us (see e.g. HardwareScalerActivity).
//mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
//mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
}
/**
* Creates an off-screen surface.
*/
public void createOffscreenSurface(int width, int height) {
if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
throw new IllegalStateException("surface already created");
}
mEGLSurface = mEglCore.createOffscreenSurface(width, height);
mWidth = width;
mHeight = height;
}
/**
* Returns the surface's width, in pixels.
* <p>
* If this is called on a window surface, and the underlying surface is in the process
* of changing size, we may not see the new size right away (e.g. in the "surfaceChanged"
* callback). The size should match after the next buffer swap.
*/
public int getWidth() {
if (mWidth < 0) {
return mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
} else {
return mWidth;
}
}
/**
* Returns the surface's height, in pixels.
*/
public int getHeight() {
if (mHeight < 0) {
return mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
} else {
return mHeight;
}
}
/**
* Release the EGL surface.
*/
public void releaseEglSurface() {
mEglCore.releaseSurface(mEGLSurface);
mEGLSurface = EGL14.EGL_NO_SURFACE;
mWidth = mHeight = -1;
}
/**
* Makes our EGL context and surface current.
*/
public void makeCurrent() {
mEglCore.makeCurrent(mEGLSurface);
}
/**
* Makes our EGL context and surface current for drawing, using the supplied surface
* for reading.
*/
public void makeCurrentReadFrom(EglBaseSurface readSurface) {
mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface);
}
/**
* Calls eglSwapBuffers. Use this to "publish" the current frame.
*
* @return false on failure
*/
public boolean swapBuffers() {
boolean result = mEglCore.swapBuffers(mEGLSurface);
if (!result) {
Log.d(TAG, "WARNING: swapBuffers() failed");
}
return result;
}
/**
* Sends the presentation time stamp to EGL.
* https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_presentation_time.txt
*
* @param nsecs Timestamp, in nanoseconds.
*/
public void setPresentationTime(long nsecs) {
mEglCore.setPresentationTime(mEGLSurface, nsecs);
}
/**
* Saves the EGL surface to a file.
* <p>
* Expects that this object's EGL surface is current.
*/
public void saveFrameToFile(File file) throws IOException {
if (!mEglCore.isCurrent(mEGLSurface)) {
throw new RuntimeException("Expected EGL context/surface is not current");
}
// glReadPixels fills in a "direct" ByteBuffer with what is essentially big-endian RGBA
// data (i.e. a byte of red, followed by a byte of green...). While the Bitmap
// constructor that takes an int[] wants little-endian ARGB (blue/red swapped), the
// Bitmap "copy pixels" method wants the same format GL provides.
//
// Ideally we'd have some way to re-use the ByteBuffer, especially if we're calling
// here often.
//
// Making this even more interesting is the upside-down nature of GL, which means
// our output will look upside down relative to what appears on screen if the
// typical GL conventions are used.
String filename = file.toString();
int width = getWidth();
int height = getHeight();
ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4);
buf.order(ByteOrder.LITTLE_ENDIAN);
GLES20.glReadPixels(0, 0, width, height,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
GlUtils.checkError("glReadPixels");
buf.rewind();
BufferedOutputStream bos = null;
try {
bos = new BufferedOutputStream(new FileOutputStream(filename));
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bmp.copyPixelsFromBuffer(buf);
bmp.compress(Bitmap.CompressFormat.PNG, 90, bos);
bmp.recycle();
} finally {
if (bos != null) bos.close();
}
}
/**
* Saves the EGL surface to given format.
* <p>
* Expects that this object's EGL surface is current.
*/
public byte[] saveFrameTo(Bitmap.CompressFormat compressFormat) {
if (!mEglCore.isCurrent(mEGLSurface)) {
throw new RuntimeException("Expected EGL context/surface is not current");
}
// glReadPixels fills in a "direct" ByteBuffer with what is essentially big-endian RGBA
// data (i.e. a byte of red, followed by a byte of green...). While the Bitmap
// constructor that takes an int[] wants little-endian ARGB (blue/red swapped), the
// Bitmap "copy pixels" method wants the same format GL provides.
//
// Ideally we'd have some way to re-use the ByteBuffer, especially if we're calling
// here often.
//
// Making this even more interesting is the upside-down nature of GL, which means
// our output will look upside down relative to what appears on screen if the
// typical GL conventions are used.
int width = getWidth();
int height = getHeight();
ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4);
buf.order(ByteOrder.LITTLE_ENDIAN);
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE,
buf);
GlUtils.checkError("glReadPixels");
buf.rewind();
ByteArrayOutputStream bos = new ByteArrayOutputStream(buf.array().length);
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bmp.copyPixelsFromBuffer(buf);
bmp.compress(compressFormat, 90, bos);
bmp.recycle();
return bos.toByteArray();
}
}

@ -1,364 +0,0 @@
/*
* Copyright 2013 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.otaliastudios.cameraview.internal.egl;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLExt;
import android.opengl.EGLSurface;
import android.os.Build;
import androidx.annotation.RequiresApi;
import android.util.Log;
import android.view.Surface;
/**
* -- from grafika --
*
* Core EGL state (display, context, config).
* <p>
* The EGLContext must only be attached to one thread at a time. This class is not thread-safe.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public final class EglCore {
private static final String TAG = EglCore.class.getSimpleName();
/**
* Constructor flag: surface must be recordable. This discourages EGL from using a
* pixel format that cannot be converted efficiently to something usable by the video
* encoder.
*/
public static final int FLAG_RECORDABLE = 0x01;
/**
* Constructor flag: ask for GLES3, fall back to GLES2 if not available. Without this
* flag, GLES2 is used.
*/
public static final int FLAG_TRY_GLES3 = 0x02;
// Android-specific extension.
private static final int EGL_RECORDABLE_ANDROID = 0x3142;
private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
private EGLConfig mEGLConfig = null;
private int mGlVersion = -1;
/**
* Prepares EGL display and context.
* <p>
* Equivalent to EglCore(null, 0).
*/
public EglCore() {
this(null, 0);
}
/**
* Prepares EGL display and context.
* <p>
* @param sharedContext The context to share, or null if sharing is not desired.
* @param flags Configuration bit flags, e.g. FLAG_RECORDABLE.
*/
public EglCore(EGLContext sharedContext, int flags) {
if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("EGL already set up");
}
if (sharedContext == null) {
sharedContext = EGL14.EGL_NO_CONTEXT;
}
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("unable to get EGL14 display");
}
int[] version = new int[2];
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
mEGLDisplay = null;
throw new RuntimeException("unable to initialize EGL14");
}
// Try to get a GLES3 context, if requested.
if ((flags & FLAG_TRY_GLES3) != 0) {
//Log.d(TAG, "Trying GLES 3");
EGLConfig config = getConfig(flags, 3);
if (config != null) {
int[] attrib3_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
EGL14.EGL_NONE
};
EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
attrib3_list, 0);
if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
//Log.d(TAG, "Got GLES 3 config");
mEGLConfig = config;
mEGLContext = context;
mGlVersion = 3;
}
}
}
if (mEGLContext == EGL14.EGL_NO_CONTEXT) { // GLES 2 only, or GLES 3 attempt failed
//Log.d(TAG, "Trying GLES 2");
EGLConfig config = getConfig(flags, 2);
if (config == null) {
throw new RuntimeException("Unable to find a suitable EGLConfig");
}
int[] attrib2_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
};
EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
attrib2_list, 0);
checkEglError("eglCreateContext");
mEGLConfig = config;
mEGLContext = context;
mGlVersion = 2;
}
// Confirm with query.
int[] values = new int[1];
EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
values, 0);
// Log.d(TAG, "EGLContext created, client version " + values[0]);
}
/**
* Finds a suitable EGLConfig.
*
* @param flags Bit flags from constructor.
* @param version Must be 2 or 3.
*/
private EGLConfig getConfig(int flags, int version) {
int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
if (version >= 3) {
renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
}
// The actual surface is generally RGBA or RGBX, so situationally omitting alpha
// doesn't really help. It can also lead to a huge performance hit on glReadPixels()
// when reading into a GL_RGBA buffer.
int[] attribList = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
//EGL14.EGL_DEPTH_SIZE, 16,
//EGL14.EGL_STENCIL_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, renderableType,
EGL14.EGL_NONE, 0, // placeholder for recordable [@-3]
EGL14.EGL_NONE
};
if ((flags & FLAG_RECORDABLE) != 0) {
attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID;
attribList[attribList.length - 2] = 1;
}
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0,
configs.length, numConfigs, 0)) {
Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
return null;
}
return configs[0];
}
/**
* Discards all resources held by this class, notably the EGL context. This must be
* called from the thread where the context was created.
* <p>
* On completion, no context will be current.
*/
public void release() {
if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
// Android is unusual in that it uses a reference-counted EGLDisplay. So for
// every eglInitialize() we need an eglTerminate().
EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
EGL14.EGL_NO_CONTEXT);
EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
EGL14.eglReleaseThread();
EGL14.eglTerminate(mEGLDisplay);
}
mEGLDisplay = EGL14.EGL_NO_DISPLAY;
mEGLContext = EGL14.EGL_NO_CONTEXT;
mEGLConfig = null;
}
@Override
protected void finalize() throws Throwable {
try {
if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
// We're limited here -- finalizers don't run on the thread that holds
// the EGL state, so if a surface or context is still current on another
// thread we can't fully release it here. Exceptions thrown from here
// are quietly discarded. Complain in the log file.
Log.w(TAG, "WARNING: EglCore was not explicitly released! " +
"State may be leaked");
release();
}
} finally {
super.finalize();
}
}
/**
* Destroys the specified surface. Note the EGLSurface won't actually be destroyed if it's
* still current in a context.
*/
public void releaseSurface(EGLSurface eglSurface) {
EGL14.eglDestroySurface(mEGLDisplay, eglSurface);
}
/**
* Creates an EGL surface associated with a Surface.
* <p>
* If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute.
*/
public EGLSurface createWindowSurface(Object surface) {
if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
throw new RuntimeException("invalid surface: " + surface);
}
// Create a window surface, and attach it to the Surface we received.
int[] surfaceAttribs = {
EGL14.EGL_NONE
};
EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
surfaceAttribs, 0);
checkEglError("eglCreateWindowSurface");
if (eglSurface == null) {
throw new RuntimeException("surface was null");
}
return eglSurface;
}
/**
* Creates an EGL surface associated with an offscreen buffer.
*/
public EGLSurface createOffscreenSurface(int width, int height) {
int[] surfaceAttribs = {
EGL14.EGL_WIDTH, width,
EGL14.EGL_HEIGHT, height,
EGL14.EGL_NONE
};
EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig,
surfaceAttribs, 0);
checkEglError("eglCreatePbufferSurface");
if (eglSurface == null) {
throw new RuntimeException("surface was null");
}
return eglSurface;
}
/**
* Makes our EGL context current, using the supplied surface for both "draw" and "read".
*/
public void makeCurrent(EGLSurface eglSurface) {
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
// called makeCurrent() before create?
// Log.d(TAG, "NOTE: makeCurrent w/o display");
}
if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) {
throw new RuntimeException("eglMakeCurrent failed");
}
}
/**
* Makes our EGL context current, using the supplied "draw" and "read" surfaces.
*/
public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) {
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
// called makeCurrent() before create?
Log.d(TAG, "NOTE: makeCurrent w/o display");
}
if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) {
throw new RuntimeException("eglMakeCurrent(draw,read) failed");
}
}
/**
* Makes no context current.
*/
public void makeNothingCurrent() {
if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
EGL14.EGL_NO_CONTEXT)) {
throw new RuntimeException("eglMakeCurrent failed");
}
}
/**
* Calls eglSwapBuffers. Use this to "publish" the current frame.
*
* @return false on failure
*/
public boolean swapBuffers(EGLSurface eglSurface) {
return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface);
}
/**
* Sends the presentation time stamp to EGL. Time is expressed in nanoseconds.
* https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_presentation_time.txt
*/
public void setPresentationTime(EGLSurface eglSurface, long nsecs) {
EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs);
}
/**
* Returns true if our context and the specified surface are current.
*/
public boolean isCurrent(EGLSurface eglSurface) {
return mEGLContext.equals(EGL14.eglGetCurrentContext()) &&
eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW));
}
/**
* Performs a simple surface query.
*/
public int querySurface(EGLSurface eglSurface, int what) {
int[] value = new int[1];
EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0);
return value[0];
}
/**
* Queries a string value.
*/
public String queryString(int what) {
return EGL14.eglQueryString(mEGLDisplay, what);
}
/**
* Returns the GLES version this context is configured for (currently 2 or 3).
*/
public int getGlVersion() {
return mGlVersion;
}
/**
* Checks for EGL errors. Throws an exception if an error has been raised.
*/
private void checkEglError(String msg) {
int error;
if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
}
}
}

@ -1,103 +0,0 @@
package com.otaliastudios.cameraview.internal.egl;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
public class EglViewport {
private final static CameraLogger LOG = CameraLogger.create(EglViewport.class.getSimpleName());
private int mProgramHandle = -1;
private int mTextureTarget;
private int mTextureUnit;
private Filter mFilter;
private Filter mPendingFilter;
public EglViewport() {
this(new NoFilter());
}
public EglViewport(@NonNull Filter filter) {
mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
mTextureUnit = GLES20.GL_TEXTURE0;
mFilter = filter;
createProgram();
}
private void createProgram() {
mProgramHandle = GlUtils.createProgram(mFilter.getVertexShader(),
mFilter.getFragmentShader());
mFilter.onCreate(mProgramHandle);
}
public void release() {
if (mProgramHandle != -1) {
mFilter.onDestroy();
GLES20.glDeleteProgram(mProgramHandle);
mProgramHandle = -1;
}
}
public int createTexture() {
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
GlUtils.checkError("glGenTextures");
int texId = textures[0];
GLES20.glActiveTexture(mTextureUnit);
GLES20.glBindTexture(mTextureTarget, texId);
GlUtils.checkError("glBindTexture " + texId);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE);
GlUtils.checkError("glTexParameter");
return texId;
}
public void setFilter(@NonNull Filter filter) {
// TODO see if this is needed. If setFilter is always called from the correct GL thread,
// we don't need to wait for a new draw call (which might not even happen).
mPendingFilter = filter;
}
public void drawFrame(long timestampUs, int textureId, float[] textureMatrix) {
if (mPendingFilter != null) {
release();
mFilter = mPendingFilter;
mPendingFilter = null;
createProgram();
}
GlUtils.checkError("draw start");
// Select the program and the active texture.
GLES20.glUseProgram(mProgramHandle);
GlUtils.checkError("glUseProgram");
GLES20.glActiveTexture(mTextureUnit);
GLES20.glBindTexture(mTextureTarget, textureId);
// Draw.
mFilter.draw(timestampUs, textureMatrix);
// Release.
GLES20.glBindTexture(mTextureTarget, 0);
GLES20.glUseProgram(0);
}
}

@ -1,80 +0,0 @@
/*
* Copyright 2013 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.otaliastudios.cameraview.internal.egl;
import android.graphics.SurfaceTexture;
import android.os.Build;
import androidx.annotation.RequiresApi;
import android.view.Surface;
/**
* Recordable EGL window surface.
* <p>
* It's good practice to explicitly release() the surface, preferably from a "finally" block.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class EglWindowSurface extends EglBaseSurface {
private Surface mSurface;
private boolean mReleaseSurface;
/**
* Associates an EGL surface with the native window surface.
* <p>
* Set releaseSurface to true if you want the Surface to be released when release() is
* called. This is convenient, but can interfere with framework classes that expect to
* manage the Surface themselves (e.g. if you release a SurfaceView's Surface, the
* surfaceDestroyed() callback won't fire).
*/
public EglWindowSurface(EglCore eglCore, Surface surface, boolean releaseSurface) {
super(eglCore);
createWindowSurface(surface);
mSurface = surface;
mReleaseSurface = releaseSurface;
}
/**
* Associates an EGL surface with the SurfaceTexture.
*/
public EglWindowSurface(EglCore eglCore, SurfaceTexture surfaceTexture) {
super(eglCore);
createWindowSurface(surfaceTexture);
}
/**
* Associates an EGL surface with the Surface.
*/
public EglWindowSurface(EglCore eglCore, Surface surface) {
super(eglCore);
createWindowSurface(surface);
}
/**
* Releases any resources associated with the EGL surface (and, if configured to do so,
* with the Surface as well).
* <p>
* Does not require that the surface's EGL context be current.
*/
public void release() {
releaseEglSurface();
if (mSurface != null) {
if (mReleaseSurface) {
mSurface.release();
}
mSurface = null;
}
}
}

@ -30,4 +30,18 @@ public interface Overlay {
* @return true to draw on it
*/
boolean drawsOn(@NonNull Target target);
/**
* Sets the overlay renderer to lock and capture the hardware canvas in order
* to capture hardware accelerated views such as video players
*
* @param on enabled
*/
void setHardwareCanvasEnabled(boolean on);
/**
* Returns true if hardware canvas capture is enabled, false by default
* @return true if capturing hardware surfaces
*/
boolean getHardwareCanvasEnabled();
}

@ -6,14 +6,15 @@ import android.graphics.PorterDuff;
import android.graphics.SurfaceTexture;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.os.Build;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.internal.Issue514Workaround;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.size.Size;
import java.nio.Buffer;
@ -27,7 +28,7 @@ import java.nio.Buffer;
* - Renders this into the current EGL window: {@link #render(long)}
* - Applies the {@link Issue514Workaround} the correct way
*
* In the future we might want to use a different approach than {@link EglViewport},
* In the future we might want to use a different approach than {@link GlTextureDrawer},
* {@link SurfaceTexture} and {@link GLES11Ext#GL_TEXTURE_EXTERNAL_OES},
* for example by using a regular {@link GLES20#GL_TEXTURE_2D} that might
* be filled through {@link GLES20#glTexImage2D(int, int, int, int, int, int, int, int, Buffer)}.
@ -40,22 +41,19 @@ public class OverlayDrawer {
private static final CameraLogger LOG = CameraLogger.create(TAG);
private Overlay mOverlay;
@VisibleForTesting int mTextureId;
private SurfaceTexture mSurfaceTexture;
private Surface mSurface;
private float[] mTransform = new float[16];
@VisibleForTesting EglViewport mViewport;
@VisibleForTesting GlTextureDrawer mTextureDrawer;
private Issue514Workaround mIssue514Workaround;
private final Object mIssue514WorkaroundLock = new Object();
public OverlayDrawer(@NonNull Overlay overlay, @NonNull Size size) {
mOverlay = overlay;
mViewport = new EglViewport();
mTextureId = mViewport.createTexture();
mSurfaceTexture = new SurfaceTexture(mTextureId);
mTextureDrawer = new GlTextureDrawer();
mSurfaceTexture = new SurfaceTexture(mTextureDrawer.getTexture().getId());
mSurfaceTexture.setDefaultBufferSize(size.getWidth(), size.getHeight());
mSurface = new Surface(mSurfaceTexture);
mIssue514Workaround = new Issue514Workaround(mTextureId);
mIssue514Workaround = new Issue514Workaround(mTextureDrawer.getTexture().getId());
}
/**
@ -66,7 +64,12 @@ public class OverlayDrawer {
*/
public void draw(@NonNull Overlay.Target target) {
try {
final Canvas surfaceCanvas = mSurface.lockCanvas(null);
final Canvas surfaceCanvas;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mOverlay.getHardwareCanvasEnabled()) {
surfaceCanvas = mSurface.lockHardwareCanvas();
} else {
surfaceCanvas = mSurface.lockCanvas(null);
}
surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mOverlay.drawOn(target, surfaceCanvas);
mSurface.unlockCanvasAndPost(surfaceCanvas);
@ -77,7 +80,7 @@ public class OverlayDrawer {
mIssue514Workaround.beforeOverlayUpdateTexImage();
mSurfaceTexture.updateTexImage();
}
mSurfaceTexture.getTransformMatrix(mTransform);
mSurfaceTexture.getTransformMatrix(mTextureDrawer.getTextureTransform());
}
/**
@ -86,7 +89,7 @@ public class OverlayDrawer {
* @return the transform matrix
*/
public float[] getTransform() {
return mTransform;
return mTextureDrawer.getTextureTransform();
}
/**
@ -105,7 +108,7 @@ public class OverlayDrawer {
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
synchronized (mIssue514WorkaroundLock) {
mViewport.drawFrame(timestampUs, mTextureId, mTransform);
mTextureDrawer.draw(timestampUs);
}
}
@ -125,9 +128,9 @@ public class OverlayDrawer {
mSurface.release();
mSurface = null;
}
if (mViewport != null) {
mViewport.release();
mViewport = null;
if (mTextureDrawer != null) {
mTextureDrawer.release();
mTextureDrawer = null;
}
}
}

@ -26,6 +26,8 @@ public class OverlayLayout extends FrameLayout implements Overlay {
@VisibleForTesting Target currentTarget = Target.PREVIEW;
private boolean mHardwareCanvasEnabled;
/**
* We set {@link #setWillNotDraw(boolean)} to false even if we don't draw anything.
* This ensures that the View system will call {@link #draw(Canvas)} on us instead
@ -99,6 +101,16 @@ public class OverlayLayout extends FrameLayout implements Overlay {
return false;
}
@Override
public void setHardwareCanvasEnabled(boolean on) {
mHardwareCanvasEnabled = on;
}
@Override
public boolean getHardwareCanvasEnabled() {
return mHardwareCanvasEnabled;
}
/**
* For {@link Target#PREVIEW}, this method is called by the View hierarchy. We will
* just forward the call to super.
@ -132,7 +144,8 @@ public class OverlayLayout extends FrameLayout implements Overlay {
"canvas:", canvas.getWidth() + "x" + canvas.getHeight(),
"view:", getWidth() + "x" + getHeight(),
"widthScale:", widthScale,
"heightScale:", heightScale
"heightScale:", heightScale,
"hardwareCanvasMode:", mHardwareCanvasEnabled
);
canvas.scale(widthScale, heightScale);
dispatchDraw(canvas);

@ -1,15 +1,19 @@
package com.otaliastudios.cameraview.picture;
import android.hardware.Camera;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.internal.utils.ExifHelper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface;
import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.internal.ExifHelper;
import com.otaliastudios.cameraview.size.Size;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -41,40 +45,63 @@ public class Full1PictureRecorder extends FullPictureRecorder {
// Stopping the preview callback is important on older APIs / emulators,
// or takePicture can hang and leave the camera in a bad state.
mCamera.setPreviewCallbackWithBuffer(null);
mCamera.takePicture(
new Camera.ShutterCallback() {
@Override
public void onShutter() {
LOG.i("take(): got onShutter callback.");
dispatchOnShutter(true);
}
},
null,
null,
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, final Camera camera) {
LOG.i("take(): got picture callback.");
int exifRotation;
try {
ExifInterface exif = new ExifInterface(new ByteArrayInputStream(data));
int exifOrientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
exifRotation = ExifHelper.getOrientation(exifOrientation);
} catch (IOException e) {
exifRotation = 0;
mEngine.getFrameManager().release();
try {
mCamera.takePicture(
new Camera.ShutterCallback() {
@Override
public void onShutter() {
LOG.i("take(): got onShutter callback.");
dispatchOnShutter(true);
}
},
null,
null,
new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, final Camera camera) {
LOG.i("take(): got picture callback.");
int exifRotation;
try {
ExifInterface exif = new ExifInterface(new ByteArrayInputStream(data));
int exifOrientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
exifRotation = ExifHelper.getOrientation(exifOrientation);
} catch (IOException e) {
exifRotation = 0;
}
mResult.data = data;
mResult.rotation = exifRotation;
LOG.i("take(): starting preview again. ", Thread.currentThread());
// It's possible that by the time this callback is invoked, we're not previewing
// anymore, so check before restarting preview.
if (mEngine.getState().isAtLeast(CameraState.PREVIEW)) {
camera.setPreviewCallbackWithBuffer(mEngine);
Size previewStreamSize = mEngine.getPreviewStreamSize(Reference.SENSOR);
if (previewStreamSize == null) {
throw new IllegalStateException("Preview stream size " +
"should never be null here.");
}
// Need to re-setup the frame manager, otherwise no frames are processed
// after takePicture() is called
mEngine.getFrameManager().setUp(
mEngine.getFrameProcessingFormat(),
previewStreamSize,
mEngine.getAngles()
);
camera.startPreview();
}
dispatchResult();
}
mResult.data = data;
mResult.rotation = exifRotation;
LOG.i("take(): starting preview again. ", Thread.currentThread());
camera.setPreviewCallbackWithBuffer(mEngine);
camera.startPreview(); // This is needed, read somewhere in the docs.
dispatchResult();
}
}
);
LOG.i("take() returned.");
);
LOG.i("take() returned.");
} catch (Exception e) {
mError = e;
dispatchResult();
}
}
@Override

@ -8,6 +8,7 @@ import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
import android.media.ImageReader;
import android.os.Build;
import android.util.Log;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.PictureFormat;
@ -15,8 +16,8 @@ import com.otaliastudios.cameraview.engine.Camera2Engine;
import com.otaliastudios.cameraview.engine.action.Action;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.BaseAction;
import com.otaliastudios.cameraview.internal.utils.ExifHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.internal.ExifHelper;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
@ -86,7 +87,13 @@ public class Full2PictureRecorder extends FullPictureRecorder
public void onCaptureCompleted(@NonNull ActionHolder holder,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
try {
super.onCaptureCompleted(holder, request, result);
} catch (Exception e) {
mError = e;
dispatchResult();
}
if (mResult.format == PictureFormat.DNG) {
mDngCreator = new DngCreator(holder.getCharacteristics(this), result);
mDngCreator.setOrientation(ExifHelper.getExifOrientation(mResult.rotation));

@ -7,9 +7,9 @@ import android.hardware.Camera;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.internal.utils.CropHelper;
import com.otaliastudios.cameraview.internal.utils.RotationHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.internal.RotationHelper;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
@ -81,7 +81,7 @@ public class Snapshot1PictureRecorder extends SnapshotPictureRecorder {
// It seems that the buffers are already cleared here, so we need to allocate again.
camera.setPreviewCallbackWithBuffer(null); // Release anything left
camera.setPreviewCallbackWithBuffer(mEngine1); // Add ourselves
mEngine1.getFrameManager().setUp(mFormat, previewStreamSize);
mEngine1.getFrameManager().setUp(mFormat, previewStreamSize, mEngine1.getAngles());
}
});
}

@ -18,7 +18,7 @@ import com.otaliastudios.cameraview.engine.action.Actions;
import com.otaliastudios.cameraview.engine.action.BaseAction;
import com.otaliastudios.cameraview.engine.action.CompletionCallback;
import com.otaliastudios.cameraview.engine.lock.LockAction;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio;
/**
@ -107,9 +107,9 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder {
public Snapshot2PictureRecorder(@NonNull PictureResult.Stub stub,
@NonNull Camera2Engine engine,
@NonNull GlCameraPreview preview,
@NonNull RendererCameraPreview preview,
@NonNull AspectRatio outputRatio) {
super(stub, engine, preview, outputRatio);
super(stub, engine, preview, outputRatio, engine.getOverlay());
mHolder = engine;
mAction = Actions.sequence(

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

Loading…
Cancel
Save