Compare commits

...

40 Commits
egloo2 ... main

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
  1. 3
      .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. 4
      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. 39
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  19. 19
      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. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CamcorderProfilesTest.java
  25. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CropHelperTest.java
  26. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/OrientationHelperTest.java
  27. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/RotationHelperTest.java
  28. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/WorkerHandlerTest.java
  29. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
  30. 36
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java
  31. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
  32. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
  33. 3
      cameraview/src/main/AndroidManifest.xml
  34. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java
  35. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  36. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java
  37. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  38. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java
  39. 126
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  40. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java
  41. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/AudioCodec.java
  42. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java
  43. 90
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  44. 112
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  45. 27
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java
  46. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  47. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/LogAction.java
  48. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera1Options.java
  49. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera2Options.java
  50. 250
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java
  51. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java
  52. 99
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java
  53. 150
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java
  54. 1
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/SimpleFilter.java
  55. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java
  56. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java
  57. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java
  58. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java
  59. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java
  60. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java
  61. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java
  62. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java
  63. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java
  64. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java
  65. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java
  66. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java
  67. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java
  68. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java
  69. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java
  70. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java
  71. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/CamcorderProfiles.java
  72. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/CropHelper.java
  73. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/ExifHelper.java
  74. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/FpsRangeValidator.java
  75. 97
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlTextureDrawer.java
  76. 96
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java
  77. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/Issue514Workaround.java
  78. 53
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/OrientationHelper.java
  79. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/Pool.java
  80. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/RotationHelper.java
  81. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/WorkerHandler.java
  82. 244
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java
  83. 364
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java
  84. 103
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java
  85. 80
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java
  86. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/Overlay.java
  87. 35
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayDrawer.java
  88. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java
  89. 101
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java
  90. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java
  91. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java
  92. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java
  93. 109
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
  94. 32
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java
  95. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/FilterCameraPreview.java
  96. 70
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
  97. 23
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererCameraPreview.java
  98. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererFrameCallback.java
  99. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreview.java
  100. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.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* - Camera engine used: *camera1/camera2/both*
- Reproducible in official demo app: *yes/no* - Reproducible in official demo app: *yes/no*
- Device / Android version: *Pixel, API 28* - Device / Android version: *Pixel, API 28*
- I have read the [FAQ page](https://natario1.github.io/CameraView/about/faq): *yes/no*
### To Reproduce ### To Reproduce
Steps to reproduce the behavior, possibly in the demo app: Steps to reproduce the behavior, possibly in the demo app:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '...'
3. See error 3. See error
### Expected behavior ### Expected behavior

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

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

@ -4,39 +4,41 @@ name: Build
on: on:
push: push:
branches: branches:
- master - main
pull_request: pull_request:
env:
TRAVIS: true
jobs: jobs:
ANDROID_BASE_CHECKS: ANDROID_BASE_CHECKS:
name: Base Checks name: Base Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
java-version: 1.8 java-version: 11
distribution: temurin
cache: gradle
- name: Perform base checks - name: Perform base checks
run: ./gradlew demo:assembleDebug cameraview:javadoc run: ./gradlew demo:assembleDebug cameraview:publishToDirectory --stacktrace
ANDROID_UNIT_TESTS: ANDROID_UNIT_TESTS:
name: Unit Tests name: Unit Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
java-version: 1.8 java-version: 11
distribution: temurin
cache: gradle
- name: Execute unit tests - name: Execute unit tests
run: ./gradlew cameraview:testDebugUnitTest run: ./gradlew cameraview:runUnitTests --stacktrace
- name: Upload unit tests artifact - name: Upload unit tests artifact
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
with: with:
name: unit_tests name: unit_tests
path: ./cameraview/build/jacoco/ path: ./cameraview/build/coverage_input/unit_tests
ANDROID_EMULATOR_TESTS: ANDROID_EMULATOR_TESTS:
name: Emulator Tests name: Emulator Tests
runs-on: macOS-latest runs-on: macos-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -60,56 +62,58 @@ jobs:
EMULATOR_ARCH: x86 EMULATOR_ARCH: x86
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
java-version: 1.8 java-version: 11
distribution: temurin
cache: gradle
- name: Execute emulator tests - name: Execute emulator tests
timeout-minutes: 30 timeout-minutes: 30
uses: reactivecircus/android-emulator-runner@v2.2.0 uses: reactivecircus/android-emulator-runner@v2.21.0
with: with:
api-level: ${{ matrix.EMULATOR_API }} api-level: ${{ matrix.EMULATOR_API }}
arch: ${{ matrix.EMULATOR_ARCH }} arch: ${{ matrix.EMULATOR_ARCH }}
disable-animations: true disable-animations: true
profile: Nexus 5X profile: Nexus 5X
emulator-options: -no-snapshot -no-window -no-boot-anim -camera-back emulated -camera-front emulated -gpu swiftshader_indirect emulator-options: -no-snapshot -no-window -no-boot-anim -camera-back emulated -camera-front emulated -gpu swiftshader_indirect
emulator-build: 6110076
script: ./.github/workflows/emulator_script.sh script: ./.github/workflows/emulator_script.sh
- name: Upload emulator tests artifact - name: Upload emulator tests artifact
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
with: with:
name: emulator_tests_${{ matrix.EMULATOR_API }} name: emulator_tests_${{ matrix.EMULATOR_API }}
path: ./cameraview/build/outputs/code_coverage/debugAndroidTest/connected path: ./cameraview/build/coverage_input/android_tests
CODE_COVERAGE: CODE_COVERAGE:
name: Code Coverage Report name: Code Coverage Report
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ANDROID_UNIT_TESTS, ANDROID_EMULATOR_TESTS] needs: [ANDROID_UNIT_TESTS, ANDROID_EMULATOR_TESTS]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
java-version: 1.8 java-version: 11
distribution: temurin
cache: gradle
- name: Download unit tests artifact - name: Download unit tests artifact
uses: actions/download-artifact@v1 uses: actions/download-artifact@v1
with: with:
name: unit_tests name: unit_tests
path: ./cameraview/build/jacoco/ path: ./cameraview/build/coverage_input/unit_tests
- name: Download emulator tests artifact - name: Download emulator tests artifact
uses: actions/download-artifact@v1 uses: actions/download-artifact@v1
with: with:
# 27 is the EMULATOR_API with less SdkExclude annotations, and should have # 27 is the EMULATOR_API with less SdkExclude annotations, and should have
# the best possible coverage. # the best possible coverage.
name: emulator_tests_27 name: emulator_tests_27
path: ./cameraview/build/outputs/code_coverage/debugAndroidTest/connected path: ./cameraview/build/coverage_input/android_tests
- name: Create merged coverage report - name: Create merged coverage report
run: ./gradlew cameraview:mergeCoverageReports run: ./gradlew cameraview:computeCoverage
- name: Upload merged coverage report (GitHub) - name: Upload merged coverage report (GitHub)
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
with: with:
name: report name: report
path: ./cameraview/build/reports/mergedCoverageReport path: ./cameraview/build/coverage_output/xml
- name: Upload merged coverage report (Codecov) - name: Upload merged coverage report (Codecov)
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v1
with: with:
token: ${{ secrets.CODECOV_KEY }} file: ./cameraview/build/coverage_output/xml/*
file: ./cameraview/build/reports/mergedCoverageReport/*
fail_ci_if_error: true fail_ci_if_error: true

@ -4,17 +4,20 @@ on:
release: release:
types: [published] types: [published]
jobs: jobs:
BINTRAY_UPLOAD: MAVEN_UPLOAD:
name: Bintray Upload name: Maven Upload
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
TRAVIS: true SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
BINTRAY_USER: ${{ secrets.BINTRAY_USER }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
BINTRAY_KEY: ${{ secrets.BINTRAY_KEY }} SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
java-version: 1.8 java-version: 11
- name: Perform bintray upload distribution: temurin
run: ./gradlew bintrayUpload 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_TAGS="$ADB_TAGS CameraIntegrationTest:I MessageQueue:W MPEG4Writer:I"
adb logcat -c adb logcat -c
adb logcat $ADB_TAGS *:E -v color & 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 Copyrights for portions of project CameraView are held by WonderKiln, Inc as
part of project CameraKit-Android (https://github.com/wonderkiln/CameraKit-Android). part of project CameraKit-Android (https://github.com/wonderkiln/CameraKit-Android).
Their original license is available below, or at Their original license is available below, or at
https://github.com/wonderkiln/CameraKit-Android/blob/master/LICENSE . https://github.com/wonderkiln/CameraKit-Android/blob/main/LICENSE .
All other copyrights for project CameraView are held by Otalia Studios, 2017. All other copyrights for project CameraView are held by Otalia Studios, 2017.
------------------------------------------------------------------------------ ------------------------------------------------------------------------------

@ -1,5 +1,5 @@
[![Build Status](https://github.com/natario1/CameraView/workflows/Build/badge.svg?event=push)](https://github.com/natario1/CameraView/actions) [![Build Status](https://github.com/natario1/CameraView/workflows/Build/badge.svg?event=push)](https://github.com/natario1/CameraView/actions)
[![Code Coverage](https://codecov.io/gh/natario1/CameraView/branch/master/graph/badge.svg)](https://codecov.io/gh/natario1/CameraView) [![Code Coverage](https://codecov.io/gh/natario1/CameraView/branch/main/graph/badge.svg)](https://codecov.io/gh/natario1/CameraView)
[![Release](https://img.shields.io/github/release/natario1/CameraView.svg)](https://github.com/natario1/CameraView/releases) [![Release](https://img.shields.io/github/release/natario1/CameraView.svg)](https://github.com/natario1/CameraView/releases)
[![Issues](https://img.shields.io/github/issues-raw/natario1/CameraView.svg)](https://github.com/natario1/CameraView/issues) [![Issues](https://img.shields.io/github/issues-raw/natario1/CameraView.svg)](https://github.com/natario1/CameraView/issues)
[![Funding](https://img.shields.io/opencollective/all/CameraView.svg?colorB=r)](https://natario1.github.io/CameraView/extra/donate) [![Funding](https://img.shields.io/opencollective/all/CameraView.svg?colorB=r)](https://natario1.github.io/CameraView/extra/donate)
@ -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. addressing most of the common issues and needs, and still leaving you with flexibility where needed.
```groovy ```groovy
api 'com.otaliastudios:cameraview:2.6.1' api 'com.otaliastudios:cameraview:2.7.2'
``` ```
- Fast & reliable - Fast & reliable

@ -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.1'
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) {
archiveClassifier.set('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) {
archiveClassifier.set('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; 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.After;
import org.junit.Before; 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") @SuppressWarnings("WeakerAccess")
@ -36,19 +17,18 @@ public abstract class BaseEglTest extends BaseTest {
protected final static int HEIGHT = 100; protected final static int HEIGHT = 100;
protected EglCore eglCore; protected EglCore eglCore;
protected EglBaseSurface eglSurface; protected EglSurface eglSurface;
@Before @Before
public void setUp() { public void setUp() {
eglCore = new EglCore(null, EglCore.FLAG_RECORDABLE); eglCore = new EglCore(EGL14.EGL_NO_CONTEXT, EglCore.FLAG_RECORDABLE);
eglSurface = new EglBaseSurface(eglCore); eglSurface = new EglOffscreenSurface(eglCore, WIDTH, HEIGHT);
eglSurface.createOffscreenSurface(WIDTH, HEIGHT);
eglSurface.makeCurrent(); eglSurface.makeCurrent();
} }
@After @After
public void tearDown() { public void tearDown() {
eglSurface.releaseEglSurface(); eglSurface.release();
eglSurface = null; eglSurface = null;
eglCore.release(); eglCore.release();
eglCore = null; eglCore = null;

@ -248,7 +248,13 @@ public class CameraViewCallbacksTest extends BaseTest {
verify(listener, times(1)).onOrientationChanged(anyInt()); verify(listener, times(1)).onOrientationChanged(anyInt());
} }
// TODO: test onShutter, here or elsewhere @Test
public void testOnShutter() {
doEndOp(op, true).when(listener).onPictureShutter();
camera.mCameraCallbacks.dispatchOnPictureShutter(true);
assertNotNull(op.await(DELAY));
verify(listener, times(1)).onPictureShutter();
}
@Test @Test
public void testCameraError() { public void testCameraError() {

@ -19,6 +19,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.ControlParser; import com.otaliastudios.cameraview.controls.ControlParser;
import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
@ -54,6 +55,7 @@ import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.size.SizeSelector; import com.otaliastudios.cameraview.size.SizeSelector;
import com.otaliastudios.cameraview.size.SizeSelectors; import com.otaliastudios.cameraview.size.SizeSelectors;
import com.otaliastudios.cameraview.tools.SdkExclude;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -165,6 +167,7 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getHdr(), controls.getHdr()); assertEquals(cameraView.getHdr(), controls.getHdr());
assertEquals(cameraView.getAudio(), controls.getAudio()); assertEquals(cameraView.getAudio(), controls.getAudio());
assertEquals(cameraView.getVideoCodec(), controls.getVideoCodec()); assertEquals(cameraView.getVideoCodec(), controls.getVideoCodec());
assertEquals(cameraView.getAudioCodec(), controls.getAudioCodec());
assertEquals(cameraView.getPictureFormat(), controls.getPictureFormat()); assertEquals(cameraView.getPictureFormat(), controls.getPictureFormat());
//noinspection SimplifiableJUnitAssertion //noinspection SimplifiableJUnitAssertion
assertEquals(cameraView.getLocation(), null); assertEquals(cameraView.getLocation(), null);
@ -214,6 +217,17 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.NONE); assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.NONE);
} }
@Test
public void testGesture_enabled() {
// Ensure touch events are intercepted when at least one gesture is set
cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM);
assertTrue(cameraView.onInterceptTouchEvent(null));
// Ensure touch events are NOT intercepted when no gestures are set
cameraView.clearGesture(Gesture.PINCH);
assertFalse(cameraView.onInterceptTouchEvent(null));
}
@Test @Test
public void testGesture_enablingDisablingLayouts() { public void testGesture_enablingDisablingLayouts() {
cameraView.clearGesture(Gesture.TAP); cameraView.clearGesture(Gesture.TAP);
@ -773,6 +787,19 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.get(Audio.class), Audio.STEREO); assertEquals(cameraView.get(Audio.class), Audio.STEREO);
} }
@Test
public void testAudioCodec() {
cameraView.set(AudioCodec.DEVICE_DEFAULT);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.DEVICE_DEFAULT);
cameraView.set(AudioCodec.AAC);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.AAC);
cameraView.set(AudioCodec.HE_AAC);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.HE_AAC);
cameraView.set(AudioCodec.AAC_ELD);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.AAC_ELD);
}
@Test @Test
public void testVideoCodec() { public void testVideoCodec() {
cameraView.set(VideoCodec.H_263); cameraView.set(VideoCodec.H_263);
@ -887,6 +914,14 @@ public class CameraViewTest extends BaseTest {
cameraView.setFrameProcessingExecutors(0); cameraView.setFrameProcessingExecutors(0);
} }
@Test
public void testDrawHardwareOverlays() {
cameraView.setDrawHardwareOverlays(true);
assertTrue(cameraView.getDrawHardwareOverlays());
cameraView.setDrawHardwareOverlays(false);
assertFalse(cameraView.getDrawHardwareOverlays());
}
//endregion //endregion
//region Lists of listeners and processors //region Lists of listeners and processors
@ -1009,6 +1044,8 @@ public class CameraViewTest extends BaseTest {
verify(cameraView.mOverlayLayout, never()).generateLayoutParams(any(AttributeSet.class)); verify(cameraView.mOverlayLayout, never()).generateLayoutParams(any(AttributeSet.class));
} }
// Broke in 31 for some reason, no time to investigate but looks like a spy() issue.
@SdkExclude(minSdkVersion = 31)
@Test @Test
public void testOverlays_addOverlayView() { public void testOverlays_addOverlayView() {
cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout); cameraView.mOverlayLayout = spy(cameraView.mOverlayLayout);
@ -1033,6 +1070,8 @@ public class CameraViewTest extends BaseTest {
verify(cameraView.mOverlayLayout, never()).addView(overlay, params); 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 @Test
public void testOverlays_removeOverlayView() { public void testOverlays_removeOverlayView() {
// First add one. // First add one.

@ -4,6 +4,7 @@ package com.otaliastudios.cameraview;
import android.location.Location; import android.location.Location;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
@ -32,7 +33,8 @@ public class VideoResultTest extends BaseTest {
File file = Mockito.mock(File.class); File file = Mockito.mock(File.class);
int rotation = 90; int rotation = 90;
Size size = new Size(20, 120); Size size = new Size(20, 120);
VideoCodec codec = VideoCodec.H_263; VideoCodec videoCodec = VideoCodec.H_263;
AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
Location location = Mockito.mock(Location.class); Location location = Mockito.mock(Location.class);
boolean isSnapshot = true; boolean isSnapshot = true;
int maxDuration = 1234; int maxDuration = 1234;
@ -47,7 +49,8 @@ public class VideoResultTest extends BaseTest {
stub.file = file; stub.file = file;
stub.rotation = rotation; stub.rotation = rotation;
stub.size = size; stub.size = size;
stub.videoCodec = codec; stub.videoCodec = videoCodec;
stub.audioCodec = audioCodec;
stub.location = location; stub.location = location;
stub.isSnapshot = isSnapshot; stub.isSnapshot = isSnapshot;
stub.maxDuration = maxDuration; stub.maxDuration = maxDuration;
@ -63,7 +66,8 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getFile(), file); assertEquals(result.getFile(), file);
assertEquals(result.getRotation(), rotation); assertEquals(result.getRotation(), rotation);
assertEquals(result.getSize(), size); assertEquals(result.getSize(), size);
assertEquals(result.getVideoCodec(), codec); assertEquals(result.getVideoCodec(), videoCodec);
assertEquals(result.getAudioCodec(), audioCodec);
assertEquals(result.getLocation(), location); assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot); assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize); assertEquals(result.getMaxSize(), maxFileSize);
@ -81,7 +85,8 @@ public class VideoResultTest extends BaseTest {
FileDescriptor fileDescriptor = FileDescriptor.in; FileDescriptor fileDescriptor = FileDescriptor.in;
int rotation = 90; int rotation = 90;
Size size = new Size(20, 120); Size size = new Size(20, 120);
VideoCodec codec = VideoCodec.H_263; VideoCodec videoCodec = VideoCodec.H_263;
AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
Location location = Mockito.mock(Location.class); Location location = Mockito.mock(Location.class);
boolean isSnapshot = true; boolean isSnapshot = true;
int maxDuration = 1234; int maxDuration = 1234;
@ -96,7 +101,8 @@ public class VideoResultTest extends BaseTest {
stub.fileDescriptor = fileDescriptor; stub.fileDescriptor = fileDescriptor;
stub.rotation = rotation; stub.rotation = rotation;
stub.size = size; stub.size = size;
stub.videoCodec = codec; stub.videoCodec = videoCodec;
stub.audioCodec = audioCodec;
stub.location = location; stub.location = location;
stub.isSnapshot = isSnapshot; stub.isSnapshot = isSnapshot;
stub.maxDuration = maxDuration; stub.maxDuration = maxDuration;
@ -112,7 +118,8 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getFileDescriptor(), fileDescriptor); assertEquals(result.getFileDescriptor(), fileDescriptor);
assertEquals(result.getRotation(), rotation); assertEquals(result.getRotation(), rotation);
assertEquals(result.getSize(), size); assertEquals(result.getSize(), size);
assertEquals(result.getVideoCodec(), codec); assertEquals(result.getVideoCodec(), videoCodec);
assertEquals(result.getAudioCodec(), audioCodec);
assertEquals(result.getLocation(), location); assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot); assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize); assertEquals(result.getMaxSize(), maxFileSize);

@ -31,7 +31,7 @@ import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.size.SizeSelectors; import com.otaliastudios.cameraview.size.SizeSelectors;
import com.otaliastudios.cameraview.tools.Emulator; import com.otaliastudios.cameraview.tools.Emulator;
import com.otaliastudios.cameraview.tools.Op; 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.overlay.Overlay;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.tools.Retry; import com.otaliastudios.cameraview.tools.Retry;
@ -45,7 +45,6 @@ import androidx.test.rule.GrantPermissionRule;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
@ -1081,6 +1080,20 @@ public abstract class CameraIntegrationTest<E extends CameraBaseEngine> extends
assert15Frames(processor); 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 @Test
@Retry(emulatorOnly = true) @Retry(emulatorOnly = true)
@SdkExclude(maxSdkVersion = 22, emulatorOnly = true) @SdkExclude(maxSdkVersion = 22, emulatorOnly = true)

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

@ -8,16 +8,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest; import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.internal.egl.EglViewport; import com.otaliastudios.opengl.program.GlTextureProgram;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -91,11 +91,11 @@ public class BaseFilterTest extends BaseEglTest {
@Test @Test
public void testOnProgramCreate() { public void testOnProgramCreate() {
filter = new TestFilter(); filter = new TestFilter();
int handle = GlUtils.createProgram(filter.getVertexShader(), filter.getFragmentShader()); int handle = GlTextureProgram.create(filter.getVertexShader(), filter.getFragmentShader());
filter.onCreate(handle); filter.onCreate(handle);
assertTrue(filter.programHandle >= 0); assertNotNull(filter.program);
filter.onDestroy(); filter.onDestroy();
assertTrue(filter.programHandle < 0); assertNull(filter.program);
GLES20.glDeleteProgram(handle); GLES20.glDeleteProgram(handle);
} }
@ -111,18 +111,18 @@ public class BaseFilterTest extends BaseEglTest {
@Test @Test
public void testDraw() { public void testDraw() {
// Use an EglViewport which cares about GL setup. // Use a drawer which cares about GL setup.
filter = spy(new TestFilter()); filter = spy(new TestFilter());
EglViewport viewport = new EglViewport(filter); GlTextureDrawer drawer = new GlTextureDrawer();
int texture = viewport.createTexture(); drawer.setFilter(filter);
float[] matrix = new float[16]; float[] matrix = drawer.getTextureTransform();
viewport.drawFrame(0L, texture, matrix); drawer.draw(0L);
verify(filter, times(1)).onPreDraw(0L, matrix); verify(filter, times(1)).onPreDraw(0L, matrix);
verify(filter, times(1)).onDraw(0L); verify(filter, times(1)).onDraw(0L);
verify(filter, times(1)).onPostDraw(0L); verify(filter, times(1)).onPostDraw(0L);
viewport.release(); drawer.release();
} }
@Test(expected = RuntimeException.class) @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.BrightnessFilter;
import com.otaliastudios.cameraview.filters.DuotoneFilter; import com.otaliastudios.cameraview.filters.DuotoneFilter;
import com.otaliastudios.cameraview.filters.VignetteFilter; import com.otaliastudios.cameraview.filters.VignetteFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.internal.egl.EglViewport; import com.otaliastudios.opengl.program.GlProgram;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -127,7 +127,7 @@ public class MultiFilterTest extends BaseEglTest {
DuotoneFilter filter = spy(new DuotoneFilter()); DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter); MultiFilter multiFilter = new MultiFilter(filter);
int program = GlUtils.createProgram(multiFilter.getVertexShader(), int program = GlProgram.create(multiFilter.getVertexShader(),
multiFilter.getFragmentShader()); multiFilter.getFragmentShader());
multiFilter.onCreate(program); multiFilter.onCreate(program);
verify(filter, never()).onCreate(anyInt()); verify(filter, never()).onCreate(anyInt());
@ -142,11 +142,11 @@ public class MultiFilterTest extends BaseEglTest {
DuotoneFilter filter = spy(new DuotoneFilter()); DuotoneFilter filter = spy(new DuotoneFilter());
MultiFilter multiFilter = new MultiFilter(filter); MultiFilter multiFilter = new MultiFilter(filter);
multiFilter.setSize(WIDTH, HEIGHT); multiFilter.setSize(WIDTH, HEIGHT);
EglViewport viewport = new EglViewport(multiFilter); GlTextureDrawer drawer = new GlTextureDrawer();
int texture = viewport.createTexture(); drawer.setFilter(multiFilter);
float[] matrix = new float[16]; float[] matrix = drawer.getTextureTransform();
viewport.drawFrame(0L, texture, matrix); drawer.draw(0L);
viewport.release(); drawer.release();
// The child should have experienced the whole lifecycle. // The child should have experienced the whole lifecycle.
verify(filter, atLeastOnce()).getVertexShader(); verify(filter, atLeastOnce()).getVertexShader();
@ -165,7 +165,9 @@ public class MultiFilterTest extends BaseEglTest {
final DuotoneFilter filter2 = spy(new DuotoneFilter()); final DuotoneFilter filter2 = spy(new DuotoneFilter());
final MultiFilter multiFilter = new MultiFilter(filter1, filter2); final MultiFilter multiFilter = new MultiFilter(filter1, filter2);
multiFilter.setSize(WIDTH, HEIGHT); 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]; final int[] result = new int[1];
doAnswer(new Answer() { doAnswer(new Answer() {
@ -173,11 +175,11 @@ public class MultiFilterTest extends BaseEglTest {
public Object answer(InvocationOnMock invocation) { public Object answer(InvocationOnMock invocation) {
MultiFilter.State state = multiFilter.states.get(filter1); MultiFilter.State state = multiFilter.states.get(filter1);
assertNotNull(state); assertNotNull(state);
assertTrue(state.isCreated); assertTrue(state.isProgramCreated);
assertTrue(state.isFramebufferCreated); assertTrue(state.isFramebufferCreated);
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0); GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
assertTrue(result[0] != 0); // assertTrue(result[0] != 0);
return null; return null;
} }
}).when(filter1).draw(0L, matrix); }).when(filter1).draw(0L, matrix);
@ -189,7 +191,7 @@ public class MultiFilterTest extends BaseEglTest {
// The last filter has no FBO / texture. // The last filter has no FBO / texture.
MultiFilter.State state = multiFilter.states.get(filter2); MultiFilter.State state = multiFilter.states.get(filter2);
assertNotNull(state); assertNotNull(state);
assertTrue(state.isCreated); assertTrue(state.isProgramCreated);
assertFalse(state.isFramebufferCreated); assertFalse(state.isFramebufferCreated);
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0); 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)); }).when(filter2).draw(eq(0L), any(float[].class));
EglViewport viewport = new EglViewport(multiFilter); drawer.draw(0L);
int texture = viewport.createTexture(); drawer.release();
viewport.drawFrame(0L, texture, matrix);
viewport.release();
// Verify that both are drawn. // Verify that both are drawn.
verify(filter1, times(1)).draw(0L, matrix); verify(filter1, times(1)).draw(0L, matrix);

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal;
import android.media.CamcorderProfile; import android.media.CamcorderProfile;
@ -8,6 +8,7 @@ import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraUtils; import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.internal.CamcorderProfiles;
import com.otaliastudios.cameraview.tools.SdkExclude; import com.otaliastudios.cameraview.tools.SdkExclude;
import com.otaliastudios.cameraview.size.Size; 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 android.graphics.Rect;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -6,6 +6,7 @@ import androidx.test.filters.SmallTest;
import android.view.OrientationEventListener; import android.view.OrientationEventListener;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.OrientationHelper;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;

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

@ -1,9 +1,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.Task;
import com.google.android.gms.tasks.Tasks; import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.tools.Op; import com.otaliastudios.cameraview.tools.Op;
import androidx.annotation.NonNull; 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 * Not clear why, but for some reason on API 28+ the UiThreadTests here crash for an internal NPE
* in FrameLayout.onMeasure. * in FrameLayout.onMeasure.
*/ */
@SdkExclude(minSdkVersion = 28, maxSdkVersion = 29) @SdkExclude(minSdkVersion = 28)
@TargetApi(17) @TargetApi(17)
public class MarkerLayoutTest extends BaseTest { public class MarkerLayoutTest extends BaseTest {

@ -1,48 +1,29 @@
package com.otaliastudios.cameraview.overlay; package com.otaliastudios.cameraview.overlay;
import android.content.res.XmlResourceParser;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Xml;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest; import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.internal.GlTextureDrawer;
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.size.Size; import com.otaliastudios.cameraview.size.Size;
import org.hamcrest.BaseMatcher; import org.hamcrest.BaseMatcher;
import org.hamcrest.Description; import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
@ -79,22 +60,19 @@ public class OverlayDrawerTest extends BaseEglTest {
@Test @Test
public void testRender() { public void testRender() {
OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT)); OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
drawer.mViewport = spy(drawer.mViewport); drawer.mTextureDrawer = spy(drawer.mTextureDrawer);
drawer.draw(Overlay.Target.PICTURE_SNAPSHOT); drawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
drawer.render(0L); drawer.render(0L);
verify(drawer.mViewport, times(1)).drawFrame( verify(drawer.mTextureDrawer, times(1)).draw(0L);
0L,
drawer.mTextureId,
drawer.getTransform()
);
} }
@Test @Test
public void testRelease() { public void testRelease() {
OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT)); OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
EglViewport viewport = spy(drawer.mViewport); GlTextureDrawer textureDrawer = spy(drawer.mTextureDrawer);
drawer.mViewport = viewport; drawer.mTextureDrawer = textureDrawer;
drawer.release(); drawer.release();
verify(viewport, times(1)).release(); verify(textureDrawer, times(1)).release();
} }
} }

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

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

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

@ -12,7 +12,7 @@ public interface BitmapCallback {
/** /**
* Notifies that the bitmap was successfully decoded. * Notifies that the bitmap was successfully decoded.
* This is run on the UI thread. * This is run on the UI thread.
* Returns a null object if a {@link OutOfMemoryError} was encountered. * Returns a null object if an {@link OutOfMemoryError} was encountered.
* *
* @param bitmap decoded bitmap, or null * @param bitmap decoded bitmap, or null
*/ */

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

@ -2,44 +2,30 @@ package com.otaliastudios.cameraview;
import android.graphics.ImageFormat; import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Range;
import android.util.Rational;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Control; import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.controls.Engine; import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.controls.Preview; 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.gesture.GestureAction;
import com.otaliastudios.cameraview.controls.Grid; import com.otaliastudios.cameraview.controls.Grid;
import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.internal.utils.CamcorderProfiles;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -120,6 +106,8 @@ public abstract class CameraOptions {
return (Collection<T>) Arrays.asList(Mode.values()); return (Collection<T>) Arrays.asList(Mode.values());
} else if (controlClass.equals(VideoCodec.class)) { } else if (controlClass.equals(VideoCodec.class)) {
return (Collection<T>) Arrays.asList(VideoCodec.values()); return (Collection<T>) Arrays.asList(VideoCodec.values());
} else if (controlClass.equals(AudioCodec.class)) {
return (Collection<T>) Arrays.asList(AudioCodec.values());
} else if (controlClass.equals(WhiteBalance.class)) { } else if (controlClass.equals(WhiteBalance.class)) {
return (Collection<T>) getSupportedWhiteBalance(); return (Collection<T>) getSupportedWhiteBalance();
} else if (controlClass.equals(Engine.class)) { } else if (controlClass.equals(Engine.class)) {

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

@ -33,6 +33,7 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent; import androidx.lifecycle.OnLifecycleEvent;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Control; import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.controls.ControlParser; import com.otaliastudios.cameraview.controls.ControlParser;
import com.otaliastudios.cameraview.controls.Engine; 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.ScrollGestureFinder;
import com.otaliastudios.cameraview.gesture.TapGestureFinder; import com.otaliastudios.cameraview.gesture.TapGestureFinder;
import com.otaliastudios.cameraview.internal.GridLinesLayout; import com.otaliastudios.cameraview.internal.GridLinesLayout;
import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.internal.utils.OrientationHelper; import com.otaliastudios.cameraview.internal.OrientationHelper;
import com.otaliastudios.cameraview.markers.AutoFocusMarker; import com.otaliastudios.cameraview.markers.AutoFocusMarker;
import com.otaliastudios.cameraview.markers.AutoFocusTrigger; import com.otaliastudios.cameraview.markers.AutoFocusTrigger;
import com.otaliastudios.cameraview.markers.MarkerLayout; import com.otaliastudios.cameraview.markers.MarkerLayout;
@ -132,6 +133,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
private Engine mEngine; private Engine mEngine;
private Filter mPendingFilter; private Filter mPendingFilter;
private int mFrameProcessingExecutors; private int mFrameProcessingExecutors;
private int mActiveGestures;
// Components // Components
private Handler mUiHandler; private Handler mUiHandler;
@ -225,6 +227,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
int frameExecutors = a.getInteger(R.styleable.CameraView_cameraFrameProcessingExecutors, int frameExecutors = a.getInteger(R.styleable.CameraView_cameraFrameProcessingExecutors,
DEFAULT_FRAME_PROCESSING_EXECUTORS); DEFAULT_FRAME_PROCESSING_EXECUTORS);
boolean drawHardwareOverlays = a.getBoolean(R.styleable.CameraView_cameraDrawHardwareOverlays, false);
// Size selectors and gestures // Size selectors and gestures
SizeSelectorParser sizeSelectors = new SizeSelectorParser(a); SizeSelectorParser sizeSelectors = new SizeSelectorParser(a);
GestureParser gestures = new GestureParser(a); GestureParser gestures = new GestureParser(a);
@ -258,6 +262,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setUseDeviceOrientation(useDeviceOrientation); setUseDeviceOrientation(useDeviceOrientation);
setGrid(controls.getGrid()); setGrid(controls.getGrid());
setGridColor(gridColor); setGridColor(gridColor);
setDrawHardwareOverlays(drawHardwareOverlays);
// Apply camera engine params // Apply camera engine params
// Adding new ones? See setEngine(). // Adding new ones? See setEngine().
@ -268,6 +273,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setHdr(controls.getHdr()); setHdr(controls.getHdr());
setAudio(controls.getAudio()); setAudio(controls.getAudio());
setAudioBitRate(audioBitRate); setAudioBitRate(audioBitRate);
setAudioCodec(controls.getAudioCodec());
setPictureSize(sizeSelectors.getPictureSizeSelector()); setPictureSize(sizeSelectors.getPictureSizeSelector());
setPictureMetering(pictureMetering); setPictureMetering(pictureMetering);
setPictureSnapshotMetering(pictureSnapshotMetering); setPictureSnapshotMetering(pictureSnapshotMetering);
@ -392,12 +398,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// attached. That's why we instantiate the preview here. // attached. That's why we instantiate the preview here.
doInstantiatePreview(); doInstantiatePreview();
} }
mOrientationHelper.enable();
} }
@Override @Override
protected void onDetachedFromWindow() { protected void onDetachedFromWindow() {
if (!mInEditor) mOrientationHelper.disable();
mLastPreviewStreamSize = null; mLastPreviewStreamSize = null;
super.onDetachedFromWindow(); super.onDetachedFromWindow();
} }
@ -605,6 +609,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
mGestureMap.get(Gesture.SCROLL_VERTICAL) != none); mGestureMap.get(Gesture.SCROLL_VERTICAL) != none);
break; break;
} }
mActiveGestures = 0;
for(GestureAction act : mGestureMap.values()) {
mActiveGestures += act == GestureAction.NONE ? 0 : 1;
}
return true; return true;
} }
mapGesture(gesture, none); mapGesture(gesture, none);
@ -633,7 +643,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override @Override
public boolean onInterceptTouchEvent(MotionEvent ev) { public boolean onInterceptTouchEvent(MotionEvent ev) {
return true; // Steal our own events. // Steal our own events if gestures are enabled
return mActiveGestures > 0;
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -669,6 +680,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//noinspection ConstantConditions //noinspection ConstantConditions
switch (action) { switch (action) {
case TAKE_PICTURE_SNAPSHOT:
takePictureSnapshot();
break;
case TAKE_PICTURE: case TAKE_PICTURE:
takePicture(); takePicture();
break; break;
@ -726,6 +741,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//region Lifecycle APIs //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. * Returns whether the camera engine has started.
* @return whether the camera has started * @return whether the camera has started
@ -744,12 +768,26 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* Sets the lifecycle owner for this view. This means you don't need * Sets the lifecycle owner for this view. This means you don't need
* to call {@link #open()}, {@link #close()} or {@link #destroy()} at all. * 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 * @param owner the owner activity or fragment
*/ */
public void setLifecycleOwner(@NonNull LifecycleOwner owner) { public void setLifecycleOwner(@Nullable LifecycleOwner owner) {
if (mLifecycle != null) mLifecycle.removeObserver(this); if (owner == null) {
mLifecycle = owner.getLifecycle(); clearLifecycleObserver();
mLifecycle.addObserver(this); } else {
clearLifecycleObserver();
mLifecycle = owner.getLifecycle();
mLifecycle.addObserver(this);
}
}
private void clearLifecycleObserver() {
if (mLifecycle != null) {
mLifecycle.removeObserver(this);
mLifecycle = null;
}
} }
/** /**
@ -831,6 +869,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void close() { public void close() {
if (mInEditor) return; if (mInEditor) return;
mOrientationHelper.disable();
mCameraEngine.stop(false); mCameraEngine.stop(false);
if (mCameraPreview != null) mCameraPreview.onPause(); if (mCameraPreview != null) mCameraPreview.onPause();
} }
@ -884,6 +923,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setWhiteBalance((WhiteBalance) control); setWhiteBalance((WhiteBalance) control);
} else if (control instanceof VideoCodec) { } else if (control instanceof VideoCodec) {
setVideoCodec((VideoCodec) control); setVideoCodec((VideoCodec) control);
} else if (control instanceof AudioCodec) {
setAudioCodec((AudioCodec) control);
} else if (control instanceof Preview) { } else if (control instanceof Preview) {
setPreview((Preview) control); setPreview((Preview) control);
} else if (control instanceof Engine) { } else if (control instanceof Engine) {
@ -920,6 +961,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return (T) getWhiteBalance(); return (T) getWhiteBalance();
} else if (controlClass == VideoCodec.class) { } else if (controlClass == VideoCodec.class) {
return (T) getVideoCodec(); return (T) getVideoCodec();
} else if (controlClass == AudioCodec.class) {
return (T) getAudioCodec();
} else if (controlClass == Preview.class) { } else if (controlClass == Preview.class) {
return (T) getPreview(); return (T) getPreview();
} else if (controlClass == Engine.class) { } else if (controlClass == Engine.class) {
@ -992,6 +1035,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setHdr(oldEngine.getHdr()); setHdr(oldEngine.getHdr());
setAudio(oldEngine.getAudio()); setAudio(oldEngine.getAudio());
setAudioBitRate(oldEngine.getAudioBitRate()); setAudioBitRate(oldEngine.getAudioBitRate());
setAudioCodec(oldEngine.getAudioCodec());
setPictureSize(oldEngine.getPictureSizeSelector()); setPictureSize(oldEngine.getPictureSizeSelector());
setPictureFormat(oldEngine.getPictureFormat()); setPictureFormat(oldEngine.getPictureFormat());
setVideoSize(oldEngine.getVideoSizeSelector()); setVideoSize(oldEngine.getVideoSizeSelector());
@ -1621,6 +1665,30 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.getAudioBitRate(); 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 * Adds a {@link CameraListener} instance to be notified of all
* interesting events that happen during the camera lifecycle. * interesting events that happen during the camera lifecycle.
@ -1756,6 +1824,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* @param fileDescriptor a file descriptor where the video will be saved * @param fileDescriptor a file descriptor where the video will be saved
* @param durationMillis recording max duration * @param durationMillis recording max duration
*/ */
@SuppressWarnings("unused")
public void takeVideo(@NonNull FileDescriptor fileDescriptor, int durationMillis) { public void takeVideo(@NonNull FileDescriptor fileDescriptor, int durationMillis) {
takeVideo(null, fileDescriptor, durationMillis); takeVideo(null, fileDescriptor, durationMillis);
} }
@ -2090,6 +2159,25 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.isTakingPicture(); 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 //endregion
//region Callbacks and dispatching //region Callbacks and dispatching
@ -2170,10 +2258,18 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
@Override @Override
public void onShutter(boolean shouldPlaySound) { public void dispatchOnPictureShutter(boolean shouldPlaySound) {
if (shouldPlaySound && mPlaySounds) { if (shouldPlaySound && mPlaySounds) {
playSound(MediaActionSound.SHUTTER_CLICK); playSound(MediaActionSound.SHUTTER_CLICK);
} }
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (CameraListener listener : mListeners) {
listener.onPictureShutter();
}
}
});
} }
@Override @Override
@ -2274,12 +2370,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
@Override @Override
public void onDisplayOffsetChanged(int displayOffset, boolean willRecreate) { public void onDisplayOffsetChanged() {
LOG.i("onDisplayOffsetChanged", displayOffset, "recreate:", willRecreate); if (isOpened()) {
if (isOpened() && !willRecreate) { // We can't handle display offset (View angle) changes without restarting.
// Display offset changes when the device rotation lock is off and the activity // See comments in OrientationHelper for more information.
// 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.
LOG.w("onDisplayOffsetChanged", "restarting the camera."); LOG.w("onDisplayOffsetChanged", "restarting the camera.");
close(); close();
open(); open();

@ -3,6 +3,7 @@ package com.otaliastudios.cameraview;
import android.location.Location; import android.location.Location;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
@ -34,6 +35,7 @@ public class VideoResult {
public FileDescriptor fileDescriptor; public FileDescriptor fileDescriptor;
public Facing facing; public Facing facing;
public VideoCodec videoCodec; public VideoCodec videoCodec;
public AudioCodec audioCodec;
public Audio audio; public Audio audio;
public long maxSize; public long maxSize;
public int maxDuration; public int maxDuration;
@ -60,6 +62,7 @@ public class VideoResult {
private final FileDescriptor fileDescriptor; private final FileDescriptor fileDescriptor;
private final Facing facing; private final Facing facing;
private final VideoCodec videoCodec; private final VideoCodec videoCodec;
private final AudioCodec audioCodec;
private final Audio audio; private final Audio audio;
private final long maxSize; private final long maxSize;
private final int maxDuration; private final int maxDuration;
@ -77,6 +80,7 @@ public class VideoResult {
fileDescriptor = builder.fileDescriptor; fileDescriptor = builder.fileDescriptor;
facing = builder.facing; facing = builder.facing;
videoCodec = builder.videoCodec; videoCodec = builder.videoCodec;
audioCodec = builder.audioCodec;
audio = builder.audio; audio = builder.audio;
maxSize = builder.maxSize; maxSize = builder.maxSize;
maxDuration = builder.maxDuration; maxDuration = builder.maxDuration;
@ -173,6 +177,16 @@ public class VideoResult {
return videoCodec; return videoCodec;
} }
/**
* Returns the codec that was used to encode the audio frames.
*
* @return the audio codec
*/
@NonNull
public AudioCodec getAudioCodec() {
return audioCodec;
}
/** /**
* Returns the max file size in bytes that was set before recording, * Returns the max file size in bytes that was set before recording,
* or 0 if no constraint was set. * or 0 if no constraint was set.

@ -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 hdr;
private int audio; private int audio;
private int videoCodec; private int videoCodec;
private int audioCodec;
private int engine; private int engine;
private int pictureFormat; private int pictureFormat;
@ -38,6 +39,8 @@ public class ControlParser {
audio = array.getInteger(R.styleable.CameraView_cameraAudio, Audio.DEFAULT.value()); audio = array.getInteger(R.styleable.CameraView_cameraAudio, Audio.DEFAULT.value());
videoCodec = array.getInteger(R.styleable.CameraView_cameraVideoCodec, videoCodec = array.getInteger(R.styleable.CameraView_cameraVideoCodec,
VideoCodec.DEFAULT.value()); VideoCodec.DEFAULT.value());
audioCodec = array.getInteger(R.styleable.CameraView_cameraAudioCodec,
AudioCodec.DEFAULT.value());
engine = array.getInteger(R.styleable.CameraView_cameraEngine, Engine.DEFAULT.value()); engine = array.getInteger(R.styleable.CameraView_cameraEngine, Engine.DEFAULT.value());
pictureFormat = array.getInteger(R.styleable.CameraView_cameraPictureFormat, pictureFormat = array.getInteger(R.styleable.CameraView_cameraPictureFormat,
PictureFormat.DEFAULT.value()); PictureFormat.DEFAULT.value());
@ -84,6 +87,11 @@ public class ControlParser {
return Audio.fromValue(audio); return Audio.fromValue(audio);
} }
@NonNull
public AudioCodec getAudioCodec() {
return AudioCodec.fromValue(audioCodec);
}
@NonNull @NonNull
public VideoCodec getVideoCodec() { public VideoCodec getVideoCodec() {
return VideoCodec.fromValue(videoCodec); return VideoCodec.fromValue(videoCodec);

@ -37,13 +37,13 @@ import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.controls.Hdr; import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.metering.MeteringRegions; import com.otaliastudios.cameraview.metering.MeteringRegions;
import com.otaliastudios.cameraview.metering.MeteringTransform; import com.otaliastudios.cameraview.metering.MeteringTransform;
import com.otaliastudios.cameraview.picture.Full1PictureRecorder; import com.otaliastudios.cameraview.picture.Full1PictureRecorder;
import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder; import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder;
import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder; import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.video.Full1VideoRecorder; import com.otaliastudios.cameraview.video.Full1VideoRecorder;
@ -100,7 +100,13 @@ public class Camera1Engine extends CameraBaseEngine implements
@NonNull @NonNull
@Override @Override
protected List<Size> getPreviewStreamAvailableSizes() { protected List<Size> getPreviewStreamAvailableSizes() {
List<Camera.Size> sizes = mCamera.getParameters().getSupportedPreviewSizes(); List<Camera.Size> sizes;
try {
sizes = mCamera.getParameters().getSupportedPreviewSizes();
} catch (Exception e) {
LOG.e("getPreviewStreamAvailableSizes:", "Failed to compute preview size. Camera params is empty");
throw new CameraException(e, CameraException.REASON_FAILED_TO_START_PREVIEW);
}
List<Size> result = new ArrayList<>(sizes.size()); List<Size> result = new ArrayList<>(sizes.size());
for (Camera.Size size : sizes) { for (Camera.Size size : sizes) {
Size add = new Size(size.width, size.height); Size add = new Size(size.width, size.height);
@ -159,17 +165,31 @@ public class Camera1Engine extends CameraBaseEngine implements
LOG.e("onStartEngine:", "Failed to connect. Maybe in use by another app?"); LOG.e("onStartEngine:", "Failed to connect. Maybe in use by another app?");
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
} }
if (mCamera == null) {
LOG.e("onStartEngine:", "Failed to connect. Camera is null, maybe in use by another app or already released?");
throw new CameraException(CameraException.REASON_FAILED_TO_CONNECT);
}
mCamera.setErrorCallback(this); mCamera.setErrorCallback(this);
// Set parameters that might have been set before the camera was opened. // Set parameters that might have been set before the camera was opened.
LOG.i("onStartEngine:", "Applying default parameters."); LOG.i("onStartEngine:", "Applying default parameters.");
Camera.Parameters params = mCamera.getParameters(); try {
mCameraOptions = new Camera1Options(params, mCameraId, Camera.Parameters params = mCamera.getParameters();
getAngles().flip(Reference.SENSOR, Reference.VIEW)); mCameraOptions = new Camera1Options(params, mCameraId,
applyAllParameters(params); getAngles().flip(Reference.SENSOR, Reference.VIEW));
mCamera.setParameters(params); applyAllParameters(params);
mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW, mCamera.setParameters(params);
Axis.ABSOLUTE)); // <- not allowed during preview } catch (Exception e) {
LOG.e("onStartEngine:", "Failed to connect. Problem with camera params");
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
}
try {
mCamera.setDisplayOrientation(getAngles().offset(Reference.SENSOR, Reference.VIEW,
Axis.ABSOLUTE)); // <- not allowed during preview
} catch (Exception e) {
LOG.e("onStartEngine:", "Failed to connect. Can't set display orientation, maybe preview already exists?");
throw new CameraException(CameraException.REASON_FAILED_TO_CONNECT);
}
LOG.i("onStartEngine:", "Ended"); LOG.i("onStartEngine:", "Ended");
return Tasks.forResult(mCameraOptions); return Tasks.forResult(mCameraOptions);
} }
@ -194,6 +214,7 @@ public class Camera1Engine extends CameraBaseEngine implements
mCaptureSize = computeCaptureSize(); mCaptureSize = computeCaptureSize();
mPreviewStreamSize = computePreviewStreamSize(); mPreviewStreamSize = computePreviewStreamSize();
LOG.i("onStartBind:", "Returning");
return Tasks.forResult(null); return Tasks.forResult(null);
} }
@ -209,8 +230,15 @@ public class Camera1Engine extends CameraBaseEngine implements
throw new IllegalStateException("previewStreamSize should not be null at this point."); throw new IllegalStateException("previewStreamSize should not be null at this point.");
} }
mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight()); mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight());
mPreview.setDrawRotation(0);
Camera.Parameters params = mCamera.getParameters(); Camera.Parameters params;
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 // NV21 should be the default, but let's make sure, since YuvImage will only support this
// and a few others // and a few others
params.setPreviewFormat(ImageFormat.NV21); params.setPreviewFormat(ImageFormat.NV21);
@ -228,7 +256,12 @@ public class Camera1Engine extends CameraBaseEngine implements
Size pictureSize = computeCaptureSize(Mode.PICTURE); Size pictureSize = computeCaptureSize(Mode.PICTURE);
params.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); 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(null); // Release anything left
mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves mCamera.setPreviewCallbackWithBuffer(this); // Add ourselves
@ -357,11 +390,12 @@ public class Camera1Engine extends CameraBaseEngine implements
LOG.i("onTakePictureSnapshot:", "executing."); LOG.i("onTakePictureSnapshot:", "executing.");
// Not the real size: it will be cropped to match the view ratio // Not the real size: it will be cropped to match the view ratio
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
// Actually it will be rotated and set to 0. if (mPreview instanceof RendererCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR); stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { mPictureRecorder = new SnapshotGlPictureRecorder(stub, this,
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio); (RendererCameraPreview) mPreview, outputRatio, getOverlay());
} else { } else {
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio); mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio);
} }
mPictureRecorder.take(); mPictureRecorder.take();
@ -397,13 +431,13 @@ public class Camera1Engine extends CameraBaseEngine implements
@Override @Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub,
@NonNull AspectRatio outputRatio) { @NonNull AspectRatio outputRatio) {
if (!(mPreview instanceof GlCameraPreview)) { if (!(mPreview instanceof RendererCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE."); throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE.");
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
throw new IllegalStateException("Video snapshots are only supported on API 18+."); throw new IllegalStateException("Video snapshots are only supported on API 18+.");
} }
GlCameraPreview glPreview = (GlCameraPreview) mPreview; RendererCameraPreview glPreview = (RendererCameraPreview) mPreview;
Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
if (outputSize == null) { if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null."); throw new IllegalStateException("outputSize should not be null.");
@ -425,8 +459,7 @@ public class Camera1Engine extends CameraBaseEngine implements
LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size); LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size);
// Start. // Start.
mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, getOverlay());
getOverlay(), stub.rotation);
mVideoRecorder.start(stub); mVideoRecorder.start(stub);
} }
@ -591,7 +624,9 @@ public class Camera1Engine extends CameraBaseEngine implements
public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) { public void setZoom(final float zoom, @Nullable final PointF[] points, final boolean notify) {
final float old = mZoomValue; final float old = mZoomValue;
mZoomValue = zoom; mZoomValue = zoom;
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, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -623,8 +658,10 @@ public class Camera1Engine extends CameraBaseEngine implements
@Nullable final PointF[] points, final boolean notify) { @Nullable final PointF[] points, final boolean notify) {
final float old = mExposureCorrectionValue; final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue; mExposureCorrectionValue = EVvalue;
// 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( mExposureCorrectionTask = getOrchestrator().scheduleStateful(
"exposure correction (" + EVvalue + ")", "exposure correction",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -844,13 +881,18 @@ public class Camera1Engine extends CameraBaseEngine implements
if (maxAF > 0) params.setFocusAreas(transformed.get(maxAF, transform)); if (maxAF > 0) params.setFocusAreas(transformed.get(maxAF, transform));
if (maxAE > 0) params.setMeteringAreas(transformed.get(maxAE, transform)); if (maxAE > 0) params.setMeteringAreas(transformed.get(maxAE, transform));
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); 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); getCallback().dispatchOnFocusStart(gesture, legacyPoint);
// The auto focus callback is not guaranteed to be called, but we really want it // 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. // to be. So we remove the old runnable if still present and post a new one.
getOrchestrator().remove(JOB_FOCUS_END); 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() { new Runnable() {
@Override @Override
public void run() { public void run() {

@ -60,11 +60,12 @@ import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.frame.ImageFrameManager; import com.otaliastudios.cameraview.frame.ImageFrameManager;
import com.otaliastudios.cameraview.gesture.Gesture; 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.metering.MeteringRegions;
import com.otaliastudios.cameraview.picture.Full2PictureRecorder; import com.otaliastudios.cameraview.picture.Full2PictureRecorder;
import com.otaliastudios.cameraview.picture.Snapshot2PictureRecorder; 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.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.video.Full2VideoRecorder; import com.otaliastudios.cameraview.video.Full2VideoRecorder;
@ -228,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 * Applies the repeating request builder to the preview, assuming we actually have a preview
* running. Can be called after changing parameters to the builder. * running. Can be called after changing parameters to the builder.
@ -419,7 +429,7 @@ public class Camera2Engine extends CameraBaseEngine implements
+ mPictureFormat); + mPictureFormat);
} }
mCameraOptions = new Camera2Options(mManager, mCameraId, flip, format); mCameraOptions = new Camera2Options(mManager, mCameraId, flip, format);
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); createRepeatingRequestBuilder(getRepeatingRequestDefaultTemplate());
} catch (CameraAccessException e) { } catch (CameraAccessException e) {
task.trySetException(createCameraException(e)); task.trySetException(createCameraException(e));
return; return;
@ -469,7 +479,7 @@ public class Camera2Engine extends CameraBaseEngine implements
// Compute sizes. // Compute sizes.
// TODO preview stream should never be bigger than 1920x1080 as per // 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. // 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 // OR: pass an int into these functions to be able to take smaller dims
// when session configuration fails // when session configuration fails
@ -489,6 +499,7 @@ public class Camera2Engine extends CameraBaseEngine implements
if (outputClass == SurfaceHolder.class) { if (outputClass == SurfaceHolder.class) {
try { try {
// This must be called from the UI thread... // This must be called from the UI thread...
LOG.i("onStartBind:", "Waiting on UI thread...");
Tasks.await(Tasks.call(new Callable<Void>() { Tasks.await(Tasks.call(new Callable<Void>() {
@Override @Override
public Void call() { public Void call() {
@ -579,9 +590,15 @@ public class Camera2Engine extends CameraBaseEngine implements
@Override @Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) { public void onConfigureFailed(@NonNull CameraCaptureSession session) {
// This SHOULD be a library error so we throw a RuntimeException.
String message = LOG.e("onConfigureFailed! Session", session); 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 @Override
@ -785,17 +802,16 @@ public class Camera2Engine extends CameraBaseEngine implements
action.start(this); action.start(this);
} else { } else {
LOG.i("onTakePictureSnapshot:", "doMetering is false. Performing."); LOG.i("onTakePictureSnapshot:", "doMetering is false. Performing.");
if (!(mPreview instanceof GlCameraPreview)) { if (!(mPreview instanceof RendererCameraPreview)) {
throw new RuntimeException("takePictureSnapshot with Camera2 is only " + throw new RuntimeException("takePictureSnapshot with Camera2 is only " +
"supported with Preview.GL_SURFACE"); "supported with Preview.GL_SURFACE");
} }
// stub.size is not the real size: it will be cropped to the given ratio // 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.rotation will be set to 0 - we rotate the texture instead.
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT); stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
Axis.RELATIVE_TO_SENSOR);
mPictureRecorder = new Snapshot2PictureRecorder(stub, this, mPictureRecorder = new Snapshot2PictureRecorder(stub, this,
(GlCameraPreview) mPreview, outputRatio); (RendererCameraPreview) mPreview, outputRatio);
mPictureRecorder.take(); mPictureRecorder.take();
} }
} }
@ -910,10 +926,10 @@ public class Camera2Engine extends CameraBaseEngine implements
@Override @Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub, protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub,
@NonNull AspectRatio outputRatio) { @NonNull AspectRatio outputRatio) {
if (!(mPreview instanceof GlCameraPreview)) { if (!(mPreview instanceof RendererCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE."); throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE.");
} }
GlCameraPreview glPreview = (GlCameraPreview) mPreview; RendererCameraPreview glPreview = (RendererCameraPreview) mPreview;
Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT); Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
if (outputSize == null) { if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null."); throw new IllegalStateException("outputSize should not be null.");
@ -921,24 +937,10 @@ public class Camera2Engine extends CameraBaseEngine implements
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio); Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height()); outputSize = new Size(outputCrop.width(), outputCrop.height());
stub.size = outputSize; stub.size = outputSize;
// Vertical: 0 (270-0-0) stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
// 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.videoFrameRate = Math.round(mPreviewFrameRate); stub.videoFrameRate = Math.round(mPreviewFrameRate);
LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size); LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size);
mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getOverlay());
// 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.start(stub); mVideoRecorder.start(stub);
} }
@ -992,9 +994,9 @@ public class Camera2Engine extends CameraBaseEngine implements
@EngineThread @EngineThread
private void maybeRestorePreviewTemplateAfterVideo() { private void maybeRestorePreviewTemplateAfterVideo() {
int template = (int) mRepeatingRequestBuilder.build().getTag(); int template = (int) mRepeatingRequestBuilder.build().getTag();
if (template != CameraDevice.TEMPLATE_PREVIEW) { if (template != getRepeatingRequestDefaultTemplate()) {
try { try {
createRepeatingRequestBuilder(CameraDevice.TEMPLATE_PREVIEW); createRepeatingRequestBuilder(getRepeatingRequestDefaultTemplate());
addRepeatingRequestBuilderSurfaces(); addRepeatingRequestBuilderSurfaces();
applyRepeatingRequestBuilder(); applyRepeatingRequestBuilder();
} catch (CameraAccessException e) { } catch (CameraAccessException e) {
@ -1261,8 +1263,10 @@ public class Camera2Engine extends CameraBaseEngine implements
public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) { public void setZoom(final float zoom, final @Nullable PointF[] points, final boolean notify) {
final float old = mZoomValue; final float old = mZoomValue;
mZoomValue = zoom; 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( mZoomTask = getOrchestrator().scheduleStateful(
"zoom (" + zoom + ")", "zoom",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -1317,8 +1321,10 @@ public class Camera2Engine extends CameraBaseEngine implements
final boolean notify) { final boolean notify) {
final float old = mExposureCorrectionValue; final float old = mExposureCorrectionValue;
mExposureCorrectionValue = EVvalue; 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( mExposureCorrectionTask = getOrchestrator().scheduleStateful(
"exposure correction (" + EVvalue + ")", "exposure correction",
CameraState.ENGINE, CameraState.ENGINE,
new Runnable() { new Runnable() {
@Override @Override
@ -1376,14 +1382,13 @@ public class Camera2Engine extends CameraBaseEngine implements
protected boolean applyPreviewFrameRate(@NonNull CaptureRequest.Builder builder, protected boolean applyPreviewFrameRate(@NonNull CaptureRequest.Builder builder,
float oldPreviewFrameRate) { float oldPreviewFrameRate) {
//noinspection unchecked //noinspection unchecked
Range<Integer>[] fallback = new Range[]{};
Range<Integer>[] fpsRanges = readCharacteristic( Range<Integer>[] fpsRanges = readCharacteristic(
CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES, CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES,
fallback); new Range[]{});
sortRanges(fpsRanges); sortFrameRateRanges(fpsRanges);
if (mPreviewFrameRate == 0F) { if (mPreviewFrameRate == 0F) {
// 0F is a special value. Fallback to a reasonable default. // 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)) { if (fpsRange.contains(30) || fpsRange.contains(24)) {
builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
return true; return true;
@ -1395,7 +1400,7 @@ public class Camera2Engine extends CameraBaseEngine implements
mCameraOptions.getPreviewFrameRateMaxValue()); mCameraOptions.getPreviewFrameRateMaxValue());
mPreviewFrameRate = Math.max(mPreviewFrameRate, mPreviewFrameRate = Math.max(mPreviewFrameRate,
mCameraOptions.getPreviewFrameRateMinValue()); mCameraOptions.getPreviewFrameRateMinValue());
for (Range<Integer> fpsRange : fpsRanges) { for (Range<Integer> fpsRange : filterFrameRateRanges(fpsRanges)) {
if (fpsRange.contains(Math.round(mPreviewFrameRate))) { if (fpsRange.contains(Math.round(mPreviewFrameRate))) {
builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); builder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
return true; return true;
@ -1406,24 +1411,33 @@ public class Camera2Engine extends CameraBaseEngine implements
return false; return false;
} }
private void sortRanges(Range<Integer>[] fpsRanges) { private void sortFrameRateRanges(@NonNull Range<Integer>[] fpsRanges) {
if (getPreviewFrameRateExact() && mPreviewFrameRate != 0) { // sort by range width in ascending order final boolean ascending = getPreviewFrameRateExact() && mPreviewFrameRate != 0;
Arrays.sort(fpsRanges, new Comparator<Range<Integer>>() { Arrays.sort(fpsRanges, new Comparator<Range<Integer>>() {
@Override @Override
public int compare(Range<Integer> range1, Range<Integer> range2) { public int compare(Range<Integer> range1, Range<Integer> range2) {
if (ascending) {
return (range1.getUpper() - range1.getLower()) return (range1.getUpper() - range1.getLower())
- (range2.getUpper() - range2.getLower()); - (range2.getUpper() - range2.getLower());
} } else {
});
} else { // sort by range width in descending order
Arrays.sort(fpsRanges, new Comparator<Range<Integer>>() {
@Override
public int compare(Range<Integer> range1, Range<Integer> range2) {
return (range2.getUpper() - range2.getLower()) return (range2.getUpper() - range2.getLower())
- (range1.getUpper() - range1.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 @Override

@ -1,7 +1,5 @@
package com.otaliastudios.cameraview.engine; package com.otaliastudios.cameraview.engine;
import android.graphics.PointF;
import android.graphics.RectF;
import android.location.Location; import android.location.Location;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
@ -16,6 +14,7 @@ import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.Hdr; 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.offset.Reference;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState; import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
import com.otaliastudios.cameraview.frame.FrameManager; import com.otaliastudios.cameraview.frame.FrameManager;
import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.overlay.Overlay; import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.picture.PictureRecorder; import com.otaliastudios.cameraview.picture.PictureRecorder;
import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.preview.CameraPreview;
@ -49,6 +47,9 @@ import java.util.List;
*/ */
public abstract class CameraBaseEngine extends CameraEngine { 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 CameraPreview mPreview;
@SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions; @SuppressWarnings("WeakerAccess") protected CameraOptions mCameraOptions;
@SuppressWarnings("WeakerAccess") protected PictureRecorder mPictureRecorder; @SuppressWarnings("WeakerAccess") protected PictureRecorder mPictureRecorder;
@ -61,6 +62,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
@SuppressWarnings("WeakerAccess") protected Flash mFlash; @SuppressWarnings("WeakerAccess") protected Flash mFlash;
@SuppressWarnings("WeakerAccess") protected WhiteBalance mWhiteBalance; @SuppressWarnings("WeakerAccess") protected WhiteBalance mWhiteBalance;
@SuppressWarnings("WeakerAccess") protected VideoCodec mVideoCodec; @SuppressWarnings("WeakerAccess") protected VideoCodec mVideoCodec;
@SuppressWarnings("WeakerAccess") protected AudioCodec mAudioCodec;
@SuppressWarnings("WeakerAccess") protected Hdr mHdr; @SuppressWarnings("WeakerAccess") protected Hdr mHdr;
@SuppressWarnings("WeakerAccess") protected PictureFormat mPictureFormat; @SuppressWarnings("WeakerAccess") protected PictureFormat mPictureFormat;
@SuppressWarnings("WeakerAccess") protected Location mLocation; @SuppressWarnings("WeakerAccess") protected Location mLocation;
@ -243,6 +245,17 @@ public abstract class CameraBaseEngine extends CameraEngine {
return mVideoBitRate; return mVideoBitRate;
} }
@Override
public final void setAudioCodec(@NonNull AudioCodec codec) {
mAudioCodec = codec;
}
@NonNull
@Override
public final AudioCodec getAudioCodec() {
return mAudioCodec;
}
@Override @Override
public final void setAudioBitRate(int audioBitRate) { public final void setAudioBitRate(int audioBitRate) {
mAudioBitRate = audioBitRate; mAudioBitRate = audioBitRate;
@ -540,16 +553,16 @@ public abstract class CameraBaseEngine extends CameraEngine {
@Override @Override
public void onPictureShutter(boolean didPlaySound) { public void onPictureShutter(boolean didPlaySound) {
getCallback().onShutter(!didPlaySound); getCallback().dispatchOnPictureShutter(!didPlaySound);
} }
@Override @Override
public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) { public void onPictureResult(@Nullable PictureResult.Stub result, @Nullable Exception error) {
mPictureRecorder = null; mPictureRecorder = null;
if (result != null) { if (result != null && result.data != null) {
getCallback().dispatchOnPictureTaken(result); getCallback().dispatchOnPictureTaken(result);
} else { } 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, getCallback().dispatchError(new CameraException(error,
CameraException.REASON_PICTURE_FAILED)); CameraException.REASON_PICTURE_FAILED));
} }
@ -581,6 +594,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
} }
stub.isSnapshot = false; stub.isSnapshot = false;
stub.videoCodec = mVideoCodec; stub.videoCodec = mVideoCodec;
stub.audioCodec = mAudioCodec;
stub.location = mLocation; stub.location = mLocation;
stub.facing = mFacing; stub.facing = mFacing;
stub.audio = mAudio; stub.audio = mAudio;
@ -608,6 +622,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
stub.file = file; stub.file = file;
stub.isSnapshot = true; stub.isSnapshot = true;
stub.videoCodec = mVideoCodec; stub.videoCodec = mVideoCodec;
stub.audioCodec = mAudioCodec;
stub.location = mLocation; stub.location = mLocation;
stub.facing = mFacing; stub.facing = mFacing;
stub.videoBitRate = mVideoBitRate; stub.videoBitRate = mVideoBitRate;

@ -2,7 +2,6 @@ package com.otaliastudios.cameraview.engine;
import android.content.Context; import android.content.Context;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.RectF;
import android.location.Location; import android.location.Location;
@ -18,6 +17,7 @@ import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions; import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.orchestrator.CameraOrchestrator; import com.otaliastudios.cameraview.engine.orchestrator.CameraOrchestrator;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState; 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.engine.offset.Reference;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameManager; 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.picture.PictureRecorder;
import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
@ -116,7 +116,7 @@ public abstract class CameraEngine implements
void dispatchOnCameraOpened(@NonNull CameraOptions options); void dispatchOnCameraOpened(@NonNull CameraOptions options);
void dispatchOnCameraClosed(); void dispatchOnCameraClosed();
void onCameraPreviewStreamSizeChanged(); void onCameraPreviewStreamSizeChanged();
void onShutter(boolean shouldPlaySound); void dispatchOnPictureShutter(boolean shouldPlaySound);
void dispatchOnVideoTaken(@NonNull VideoResult.Stub stub); void dispatchOnVideoTaken(@NonNull VideoResult.Stub stub);
void dispatchOnPictureTaken(@NonNull PictureResult.Stub stub); void dispatchOnPictureTaken(@NonNull PictureResult.Stub stub);
void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where); void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where);
@ -638,6 +638,9 @@ public abstract class CameraEngine implements
public abstract void setAudioBitRate(int audioBitRate); public abstract void setAudioBitRate(int audioBitRate);
public abstract int getAudioBitRate(); public abstract int getAudioBitRate();
public abstract void setAudioCodec(@NonNull AudioCodec codec);
@NonNull public abstract AudioCodec getAudioCodec();
public abstract void setSnapshotMaxWidth(int maxWidth); public abstract void setSnapshotMaxWidth(int maxWidth);
public abstract int getSnapshotMaxWidth(); public abstract int getSnapshotMaxWidth();

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

@ -13,7 +13,7 @@ import com.otaliastudios.cameraview.controls.Hdr;
import com.otaliastudios.cameraview.controls.PictureFormat; import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.engine.mappers.Camera1Mapper; 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.AspectRatio;
import com.otaliastudios.cameraview.size.Size; 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.PictureFormat;
import com.otaliastudios.cameraview.controls.WhiteBalance; import com.otaliastudios.cameraview.controls.WhiteBalance;
import com.otaliastudios.cameraview.engine.mappers.Camera2Mapper; 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.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;

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

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

@ -1,15 +1,13 @@
package com.otaliastudios.cameraview.filter; package com.otaliastudios.cameraview.filter;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.opengl.draw.GlDrawable;
import java.nio.FloatBuffer; 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. * 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"; + "}\n";
} }
// When the model/view/projection matrix is identity, this will exactly cover the viewport. @VisibleForTesting GlTextureProgram program = null;
private FloatBuffer vertexPosition = GlUtils.floatBuffer(new float[]{ private GlDrawable programDrawable = null;
-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 Size size; @VisibleForTesting Size size;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
@ -141,28 +121,23 @@ public abstract class BaseFilter implements Filter {
@Override @Override
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
this.programHandle = programHandle; program = new GlTextureProgram(programHandle,
vertexPositionLocation = GLES20.glGetAttribLocation(programHandle, vertexPositionName); vertexPositionName,
GlUtils.checkLocation(vertexPositionLocation, vertexPositionName); vertexModelViewProjectionMatrixName,
vertexTextureCoordinateLocation = GLES20.glGetAttribLocation(programHandle, vertexTextureCoordinateName,
vertexTextureCoordinateName);
GlUtils.checkLocation(vertexTextureCoordinateLocation, vertexTextureCoordinateName);
vertexModelViewProjectionMatrixLocation = GLES20.glGetUniformLocation(programHandle,
vertexModelViewProjectionMatrixName);
GlUtils.checkLocation(vertexModelViewProjectionMatrixLocation,
vertexModelViewProjectionMatrixName);
vertexTransformMatrixLocation = GLES20.glGetUniformLocation(programHandle,
vertexTransformMatrixName); vertexTransformMatrixName);
GlUtils.checkLocation(vertexTransformMatrixLocation, vertexTransformMatrixName); programDrawable = new GlRect();
} }
@Override @Override
public void onDestroy() { public void onDestroy() {
programHandle = -1; // Since we used the handle constructor of GlTextureProgram, calling release here
vertexPositionLocation = -1; // will NOT destroy the GL program. This is important because Filters are not supposed
vertexTextureCoordinateLocation = -1; // to have ownership of programs. Creation and deletion happen outside, and deleting twice
vertexModelViewProjectionMatrixLocation = -1; // would cause an error.
vertexTransformMatrixLocation = -1; program.release();
program = null;
programDrawable = null;
} }
@NonNull @NonNull
@ -178,7 +153,7 @@ public abstract class BaseFilter implements Filter {
@Override @Override
public void draw(long timestampUs, @NonNull float[] transformMatrix) { public void draw(long timestampUs, @NonNull float[] transformMatrix) {
if (programHandle == -1) { if (program == null) {
LOG.w("Filter.draw() called after destroying the filter. " + LOG.w("Filter.draw() called after destroying the filter. " +
"This can happen rarely because of threading."); "This can happen rarely because of threading.");
} else { } else {
@ -189,43 +164,18 @@ public abstract class BaseFilter implements Filter {
} }
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
// Copy the model / view / projection matrix over. program.setTextureTransform(transformMatrix);
GLES20.glUniformMatrix4fv(vertexModelViewProjectionMatrixLocation, 1, program.onPreDraw(programDrawable, programDrawable.getModelMatrix());
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");
} }
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected void onDraw(long timestampUs) { protected void onDraw(@SuppressWarnings("unused") long timestampUs) {
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); program.onDraw(programDrawable);
GlUtils.checkError("glDrawArrays");
} }
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected void onPostDraw(long timestampUs) { protected void onPostDraw(@SuppressWarnings("unused") long timestampUs) {
GLES20.glDisableVertexAttribArray(vertexPositionLocation); program.onPostDraw(programDrawable);
GLES20.glDisableVertexAttribArray(vertexTextureCoordinateLocation);
} }
@NonNull @NonNull
@ -244,6 +194,7 @@ public abstract class BaseFilter implements Filter {
return copy; return copy;
} }
@NonNull
protected BaseFilter onCopy() { protected BaseFilter onCopy() {
try { try {
return getClass().newInstance(); return getClass().newInstance();

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

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

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter; import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; 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. * 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) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale"); Egloo.checkGlProgramLocation(scaleLocation, "scale");
} }
@Override @Override
@ -120,6 +120,6 @@ public class AutoFixFilter extends BaseFilter implements OneParameterFilter {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(scaleLocation, scale); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.opengl.core.Egloo;
/** /**
* Adjusts the brightness of the frames. * Adjusts the brightness of the frames.
@ -76,7 +76,7 @@ public class BrightnessFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
brightnessLocation = GLES20.glGetUniformLocation(programHandle, "brightness"); brightnessLocation = GLES20.glGetUniformLocation(programHandle, "brightness");
GlUtils.checkLocation(brightnessLocation, "brightness"); Egloo.checkGlProgramLocation(brightnessLocation, "brightness");
} }
@Override @Override
@ -89,6 +89,6 @@ public class BrightnessFilter extends BaseFilter implements OneParameterFilter {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(brightnessLocation, brightness); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.opengl.core.Egloo;
/** /**
* Adjusts the contrast. * Adjusts the contrast.
@ -78,7 +78,7 @@ public class ContrastFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
contrastLocation = GLES20.glGetUniformLocation(programHandle, "contrast"); contrastLocation = GLES20.glGetUniformLocation(programHandle, "contrast");
GlUtils.checkLocation(contrastLocation, "contrast"); Egloo.checkGlProgramLocation(contrastLocation, "contrast");
} }
@Override @Override
@ -91,6 +91,6 @@ public class ContrastFilter extends BaseFilter implements OneParameterFilter {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(contrastLocation, contrast); GLES20.glUniform1f(contrastLocation, contrast);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
} }
} }

@ -5,7 +5,7 @@ import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter; import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.opengl.core.Egloo;
import java.util.Random; import java.util.Random;
@ -80,9 +80,9 @@ public class DocumentaryFilter extends BaseFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(mScaleLocation, "scale"); Egloo.checkGlProgramLocation(mScaleLocation, "scale");
mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist"); mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
GlUtils.checkLocation(mMaxDistLocation, "inv_max_dist"); Egloo.checkGlProgramLocation(mMaxDistLocation, "inv_max_dist");
} }
@Override @Override
@ -104,12 +104,12 @@ public class DocumentaryFilter extends BaseFilter {
scale[1] = 1f; scale[1] = 1f;
} }
GLES20.glUniform2fv(mScaleLocation, 1, scale, 0); 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 maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
float invMaxDist = 1F / maxDist; float invMaxDist = 1F / maxDist;
GLES20.glUniform1f(mMaxDistLocation, invMaxDist); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.TwoParameterFilter; 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. * Representation of input frames using only two color tones.
@ -131,9 +131,9 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
mFirstColorLocation = GLES20.glGetUniformLocation(programHandle, "first"); mFirstColorLocation = GLES20.glGetUniformLocation(programHandle, "first");
GlUtils.checkLocation(mFirstColorLocation, "first"); Egloo.checkGlProgramLocation(mFirstColorLocation, "first");
mSecondColorLocation = GLES20.glGetUniformLocation(programHandle, "second"); mSecondColorLocation = GLES20.glGetUniformLocation(programHandle, "second");
GlUtils.checkLocation(mSecondColorLocation, "second"); Egloo.checkGlProgramLocation(mSecondColorLocation, "second");
} }
@Override @Override
@ -150,9 +150,9 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter {
Color.blue(mSecondColor) / 255f Color.blue(mSecondColor) / 255f
}; };
GLES20.glUniform3fv(mFirstColorLocation, 1, first, 0); GLES20.glUniform3fv(mFirstColorLocation, 1, first, 0);
GlUtils.checkError("glUniform3fv"); Egloo.checkGlError("glUniform3fv");
GLES20.glUniform3fv(mSecondColorLocation, 1, second, 0); GLES20.glUniform3fv(mSecondColorLocation, 1, second, 0);
GlUtils.checkError("glUniform3fv"); Egloo.checkGlError("glUniform3fv");
} }
@Override @Override

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter; import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; 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. * Applies back-light filling to the frames.
@ -82,9 +82,9 @@ public class FillLightFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
multiplierLocation = GLES20.glGetUniformLocation(programHandle, "mult"); multiplierLocation = GLES20.glGetUniformLocation(programHandle, "mult");
GlUtils.checkLocation(multiplierLocation, "mult"); Egloo.checkGlProgramLocation(multiplierLocation, "mult");
gammaLocation = GLES20.glGetUniformLocation(programHandle, "igamma"); gammaLocation = GLES20.glGetUniformLocation(programHandle, "igamma");
GlUtils.checkLocation(gammaLocation, "igamma"); Egloo.checkGlProgramLocation(gammaLocation, "igamma");
} }
@Override @Override
@ -100,12 +100,12 @@ public class FillLightFilter extends BaseFilter implements OneParameterFilter {
float amount = 1.0f - strength; float amount = 1.0f - strength;
float multiplier = 1.0f / (amount * 0.7f + 0.3f); float multiplier = 1.0f / (amount * 0.7f + 0.3f);
GLES20.glUniform1f(multiplierLocation, multiplier); GLES20.glUniform1f(multiplierLocation, multiplier);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
float fadeGamma = 0.3f; float fadeGamma = 0.3f;
float faded = fadeGamma + (1.0f - fadeGamma) * multiplier; float faded = fadeGamma + (1.0f - fadeGamma) * multiplier;
float gamma = 1.0f / faded; float gamma = 1.0f / faded;
GLES20.glUniform1f(gammaLocation, gamma); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.opengl.core.Egloo;
/** /**
* Applies gamma correction to the frames. * Applies gamma correction to the frames.
@ -73,7 +73,7 @@ public class GammaFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
gammaLocation = GLES20.glGetUniformLocation(programHandle, "gamma"); gammaLocation = GLES20.glGetUniformLocation(programHandle, "gamma");
GlUtils.checkLocation(gammaLocation, "gamma"); Egloo.checkGlProgramLocation(gammaLocation, "gamma");
} }
@Override @Override
@ -86,6 +86,6 @@ public class GammaFilter extends BaseFilter implements OneParameterFilter {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(gammaLocation, gamma); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.opengl.core.Egloo;
import java.util.Random; import java.util.Random;
@ -122,11 +122,11 @@ public class GrainFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
strengthLocation = GLES20.glGetUniformLocation(programHandle, "scale"); strengthLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(strengthLocation, "scale"); Egloo.checkGlProgramLocation(strengthLocation, "scale");
stepXLocation = GLES20.glGetUniformLocation(programHandle, "stepX"); stepXLocation = GLES20.glGetUniformLocation(programHandle, "stepX");
GlUtils.checkLocation(stepXLocation, "stepX"); Egloo.checkGlProgramLocation(stepXLocation, "stepX");
stepYLocation = GLES20.glGetUniformLocation(programHandle, "stepY"); stepYLocation = GLES20.glGetUniformLocation(programHandle, "stepY");
GlUtils.checkLocation(stepYLocation, "stepY"); Egloo.checkGlProgramLocation(stepYLocation, "stepY");
} }
@Override @Override
@ -141,10 +141,10 @@ public class GrainFilter extends BaseFilter implements OneParameterFilter {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(strengthLocation, strength); GLES20.glUniform1f(strengthLocation, strength);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepXLocation, 0.5f / width); GLES20.glUniform1f(stepXLocation, 0.5f / width);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepYLocation, 0.5f / height); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; 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. * Applies a hue effect on the input frames.
@ -86,7 +86,7 @@ public class HueFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
hueLocation = GLES20.glGetUniformLocation(programHandle, "hue"); hueLocation = GLES20.glGetUniformLocation(programHandle, "hue");
GlUtils.checkLocation(hueLocation, "hue"); Egloo.checkGlProgramLocation(hueLocation, "hue");
} }
@Override @Override
@ -101,6 +101,6 @@ public class HueFilter extends BaseFilter implements OneParameterFilter {
// map it on 360 degree circle // map it on 360 degree circle
float shaderHue = ((hue - 45) / 45f + 0.5f) * -1; float shaderHue = ((hue - 45) / 45f + 0.5f) * -1;
GLES20.glUniform1f(hueLocation, shaderHue); GLES20.glUniform1f(hueLocation, shaderHue);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
} }
} }

@ -5,7 +5,7 @@ import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter; import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.opengl.core.Egloo;
import java.util.Random; import java.util.Random;
@ -122,13 +122,13 @@ public class LomoishFilter extends BaseFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale"); Egloo.checkGlProgramLocation(scaleLocation, "scale");
maxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist"); maxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
GlUtils.checkLocation(maxDistLocation, "inv_max_dist"); Egloo.checkGlProgramLocation(maxDistLocation, "inv_max_dist");
stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX"); stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX");
GlUtils.checkLocation(stepSizeXLocation, "stepsizeX"); Egloo.checkGlProgramLocation(stepSizeXLocation, "stepsizeX");
stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY"); stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY");
GlUtils.checkLocation(stepSizeYLocation, "stepsizeY"); Egloo.checkGlProgramLocation(stepSizeYLocation, "stepsizeY");
} }
@Override @Override
@ -153,12 +153,12 @@ public class LomoishFilter extends BaseFilter {
} }
float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f; float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
GLES20.glUniform2fv(scaleLocation, 1, scale, 0); GLES20.glUniform2fv(scaleLocation, 1, scale, 0);
GlUtils.checkError("glUniform2fv"); Egloo.checkGlError("glUniform2fv");
GLES20.glUniform1f(maxDistLocation, 1.0F / maxDist); GLES20.glUniform1f(maxDistLocation, 1.0F / maxDist);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepSizeXLocation, 1.0F / width); GLES20.glUniform1f(stepSizeXLocation, 1.0F / width);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepSizeYLocation, 1.0F / height); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.opengl.core.Egloo;
/** /**
* Adjusts color saturation. * Adjusts color saturation.
@ -93,9 +93,9 @@ public class SaturationFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale"); Egloo.checkGlProgramLocation(scaleLocation, "scale");
exponentsLocation = GLES20.glGetUniformLocation(programHandle, "exponents"); exponentsLocation = GLES20.glGetUniformLocation(programHandle, "exponents");
GlUtils.checkLocation(exponentsLocation, "exponents"); Egloo.checkGlProgramLocation(exponentsLocation, "exponents");
} }
@Override @Override
@ -110,18 +110,18 @@ public class SaturationFilter extends BaseFilter implements OneParameterFilter {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
if (scale > 0.0f) { if (scale > 0.0f) {
GLES20.glUniform1f(scaleLocation, 0F); GLES20.glUniform1f(scaleLocation, 0F);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
GLES20.glUniform3f(exponentsLocation, GLES20.glUniform3f(exponentsLocation,
(0.9f * scale) + 1.0f, (0.9f * scale) + 1.0f,
(2.1f * scale) + 1.0f, (2.1f * scale) + 1.0f,
(2.7f * scale) + 1.0f (2.7f * scale) + 1.0f
); );
GlUtils.checkError("glUniform3f"); Egloo.checkGlError("glUniform3f");
} else { } else {
GLES20.glUniform1f(scaleLocation, 1.0F + scale); GLES20.glUniform1f(scaleLocation, 1.0F + scale);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
GLES20.glUniform3f(exponentsLocation, 0F, 0F, 0F); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.opengl.core.Egloo;
/** /**
* Sharpens the input frames. * Sharpens the input frames.
@ -100,11 +100,11 @@ public class SharpnessFilter extends BaseFilter implements OneParameterFilter {
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale"); Egloo.checkGlProgramLocation(scaleLocation, "scale");
stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX"); stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX");
GlUtils.checkLocation(stepSizeXLocation, "stepsizeX"); Egloo.checkGlProgramLocation(stepSizeXLocation, "stepsizeX");
stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY"); stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY");
GlUtils.checkLocation(stepSizeYLocation, "stepsizeY"); Egloo.checkGlProgramLocation(stepSizeYLocation, "stepsizeY");
} }
@Override @Override
@ -119,10 +119,10 @@ public class SharpnessFilter extends BaseFilter implements OneParameterFilter {
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(scaleLocation, scale); GLES20.glUniform1f(scaleLocation, scale);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepSizeXLocation, 1.0F / width); GLES20.glUniform1f(stepSizeXLocation, 1.0F / width);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(stepSizeYLocation, 1.0F / height); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.opengl.core.Egloo;
/** /**
* Adjusts color temperature. * Adjusts color temperature.
@ -84,7 +84,7 @@ public class TemperatureFilter extends BaseFilter implements OneParameterFilter
public void onCreate(int programHandle) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale"); Egloo.checkGlProgramLocation(scaleLocation, "scale");
} }
@Override @Override
@ -97,6 +97,6 @@ public class TemperatureFilter extends BaseFilter implements OneParameterFilter
protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
GLES20.glUniform1f(scaleLocation, scale); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter; 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) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
tintLocation = GLES20.glGetUniformLocation(programHandle, "tint"); tintLocation = GLES20.glGetUniformLocation(programHandle, "tint");
GlUtils.checkLocation(tintLocation, "tint"); Egloo.checkGlProgramLocation(tintLocation, "tint");
} }
@Override @Override
@ -99,6 +99,6 @@ public class TintFilter extends BaseFilter implements OneParameterFilter {
Color.blue(tint) / 255f Color.blue(tint) / 255f
}; };
GLES20.glUniform3fv(tintLocation, 1, channels, 0); 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.BaseFilter;
import com.otaliastudios.cameraview.filter.TwoParameterFilter; 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) { public void onCreate(int programHandle) {
super.onCreate(programHandle); super.onCreate(programHandle);
mRangeLocation = GLES20.glGetUniformLocation(programHandle, "range"); mRangeLocation = GLES20.glGetUniformLocation(programHandle, "range");
GlUtils.checkLocation(mRangeLocation, "range"); Egloo.checkGlProgramLocation(mRangeLocation, "range");
mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist"); mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
GlUtils.checkLocation(mMaxDistLocation, "inv_max_dist"); Egloo.checkGlProgramLocation(mMaxDistLocation, "inv_max_dist");
mShadeLocation = GLES20.glGetUniformLocation(programHandle, "shade"); mShadeLocation = GLES20.glGetUniformLocation(programHandle, "shade");
GlUtils.checkLocation(mShadeLocation, "shade"); Egloo.checkGlProgramLocation(mShadeLocation, "shade");
mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale"); mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(mScaleLocation, "scale"); Egloo.checkGlProgramLocation(mScaleLocation, "scale");
} }
@Override @Override
@ -156,20 +156,20 @@ public class VignetteFilter extends BaseFilter implements TwoParameterFilter {
scale[1] = 1f; scale[1] = 1f;
} }
GLES20.glUniform2fv(mScaleLocation, 1, scale, 0); 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 maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
GLES20.glUniform1f(mMaxDistLocation, 1F / maxDist); GLES20.glUniform1f(mMaxDistLocation, 1F / maxDist);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
GLES20.glUniform1f(mShadeLocation, mShade); 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 // 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 // which means no vignette at all because the luminousity difference is
// less than 1/256 and will cause nothing. // less than 1/256 and will cause nothing.
float range = (1.30f - (float) Math.sqrt(mScale) * 0.7f); float range = (1.30f - (float) Math.sqrt(mScale) * 0.7f);
GLES20.glUniform1f(mRangeLocation, range); GLES20.glUniform1f(mRangeLocation, range);
GlUtils.checkError("glUniform1f"); Egloo.checkGlError("glUniform1f");
} }
} }

@ -43,6 +43,15 @@ public enum GestureAction {
*/ */
TAKE_PICTURE(2, GestureType.ONE_SHOT), 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. * Zoom control, typically assigned to the pinch gesture.
* This action can be mapped to continuous gestures: * This action can be mapped to continuous gestures:
@ -51,7 +60,7 @@ public enum GestureAction {
* - {@link Gesture#SCROLL_HORIZONTAL} * - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL} * - {@link Gesture#SCROLL_VERTICAL}
*/ */
ZOOM(3, GestureType.CONTINUOUS), ZOOM(4, GestureType.CONTINUOUS),
/** /**
* Exposure correction control. * Exposure correction control.
@ -61,7 +70,7 @@ public enum GestureAction {
* - {@link Gesture#SCROLL_HORIZONTAL} * - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL} * - {@link Gesture#SCROLL_VERTICAL}
*/ */
EXPOSURE_CORRECTION(4, GestureType.CONTINUOUS), EXPOSURE_CORRECTION(5, GestureType.CONTINUOUS),
/** /**
* Controls the first parameter of a real-time {@link Filter}, * Controls the first parameter of a real-time {@link Filter},
@ -71,7 +80,7 @@ public enum GestureAction {
* - {@link Gesture#SCROLL_HORIZONTAL} * - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL} * - {@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}, * Controls the second parameter of a real-time {@link Filter},
@ -81,7 +90,7 @@ public enum GestureAction {
* - {@link Gesture#SCROLL_HORIZONTAL} * - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL} * - {@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_PINCH = NONE;
final static GestureAction DEFAULT_TAP = 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.annotation.SuppressLint;
import android.media.CamcorderProfile; import android.media.CamcorderProfile;

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

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal;
import androidx.exifinterface.media.ExifInterface; 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.GLES11Ext;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.view.Surface; import android.view.Surface;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.preview.RendererThread; 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 * This makes no sense, since overlaySurfaceTexture.updateTexImage() is setting it to
* overlayTextureId anyway, but it fixes the issue. Specifically, after any draw operation with * 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 #beforeOverlayUpdateTexImage()} to be called before the
* {@link SurfaceTexture#updateTexImage()} call * {@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, * 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 * 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 * REFERENCES
* https://github.com/natario1/CameraView/issues/514 * 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.Context;
import android.content.res.Configuration;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.Display; import android.view.Display;
import android.view.OrientationEventListener; import android.view.OrientationEventListener;
import android.view.Surface; 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) * 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 * and the display offset (which depends on the activity orientation wrt the device default
* orientation). * 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 { public class OrientationHelper {
@ -24,9 +59,10 @@ public class OrientationHelper {
*/ */
public interface Callback { public interface Callback {
void onDeviceOrientationChanged(int deviceOrientation); 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 Context mContext;
private final Callback mCallback; private final Callback mCallback;
@ -38,6 +74,8 @@ public class OrientationHelper {
final DisplayManager.DisplayListener mDisplayOffsetListener; final DisplayManager.DisplayListener mDisplayOffsetListener;
private int mDisplayOffset = -1; private int mDisplayOffset = -1;
private boolean mEnabled;
/** /**
* Creates a new orientation helper. * Creates a new orientation helper.
* @param context a valid context * @param context a valid context
@ -82,9 +120,7 @@ public class OrientationHelper {
int newDisplayOffset = findDisplayOffset(); int newDisplayOffset = findDisplayOffset();
if (newDisplayOffset != oldDisplayOffset) { if (newDisplayOffset != oldDisplayOffset) {
mDisplayOffset = newDisplayOffset; mDisplayOffset = newDisplayOffset;
// With 180 degrees flips, the activity is not recreated. mCallback.onDisplayOffsetChanged();
boolean willRecreate = Math.abs(newDisplayOffset - oldDisplayOffset) != 180;
mCallback.onDisplayOffsetChanged(newDisplayOffset, willRecreate);
} }
} }
}; };
@ -97,11 +133,14 @@ public class OrientationHelper {
* Enables this listener. * Enables this listener.
*/ */
public void enable() { public void enable() {
if (mEnabled) return;
mEnabled = true;
mDisplayOffset = findDisplayOffset(); mDisplayOffset = findDisplayOffset();
if (Build.VERSION.SDK_INT >= 17) { if (Build.VERSION.SDK_INT >= 17) {
DisplayManager manager = (DisplayManager) DisplayManager manager = (DisplayManager)
mContext.getSystemService(Context.DISPLAY_SERVICE); 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(); mDeviceOrientationListener.enable();
} }
@ -110,6 +149,8 @@ public class OrientationHelper {
* Disables this listener. * Disables this listener.
*/ */
public void disable() { public void disable() {
if (!mEnabled) return;
mEnabled = false;
mDeviceOrientationListener.disable(); mDeviceOrientationListener.disable();
if (Build.VERSION.SDK_INT >= 17) { if (Build.VERSION.SDK_INT >= 17) {
DisplayManager manager = (DisplayManager) DisplayManager manager = (DisplayManager)

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal;
import com.otaliastudios.cameraview.CameraLogger; 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; 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.Handler;
import android.os.HandlerThread; 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 * @return true to draw on it
*/ */
boolean drawsOn(@NonNull Target target); 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.graphics.SurfaceTexture;
import android.opengl.GLES11Ext; import android.opengl.GLES11Ext;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.os.Build;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.internal.Issue514Workaround; import com.otaliastudios.cameraview.internal.Issue514Workaround;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import java.nio.Buffer; import java.nio.Buffer;
@ -27,7 +28,7 @@ import java.nio.Buffer;
* - Renders this into the current EGL window: {@link #render(long)} * - Renders this into the current EGL window: {@link #render(long)}
* - Applies the {@link Issue514Workaround} the correct way * - 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}, * {@link SurfaceTexture} and {@link GLES11Ext#GL_TEXTURE_EXTERNAL_OES},
* for example by using a regular {@link GLES20#GL_TEXTURE_2D} that might * 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)}. * 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 static final CameraLogger LOG = CameraLogger.create(TAG);
private Overlay mOverlay; private Overlay mOverlay;
@VisibleForTesting int mTextureId;
private SurfaceTexture mSurfaceTexture; private SurfaceTexture mSurfaceTexture;
private Surface mSurface; private Surface mSurface;
private float[] mTransform = new float[16]; @VisibleForTesting GlTextureDrawer mTextureDrawer;
@VisibleForTesting EglViewport mViewport;
private Issue514Workaround mIssue514Workaround; private Issue514Workaround mIssue514Workaround;
private final Object mIssue514WorkaroundLock = new Object(); private final Object mIssue514WorkaroundLock = new Object();
public OverlayDrawer(@NonNull Overlay overlay, @NonNull Size size) { public OverlayDrawer(@NonNull Overlay overlay, @NonNull Size size) {
mOverlay = overlay; mOverlay = overlay;
mViewport = new EglViewport(); mTextureDrawer = new GlTextureDrawer();
mTextureId = mViewport.createTexture(); mSurfaceTexture = new SurfaceTexture(mTextureDrawer.getTexture().getId());
mSurfaceTexture = new SurfaceTexture(mTextureId);
mSurfaceTexture.setDefaultBufferSize(size.getWidth(), size.getHeight()); mSurfaceTexture.setDefaultBufferSize(size.getWidth(), size.getHeight());
mSurface = new Surface(mSurfaceTexture); 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) { public void draw(@NonNull Overlay.Target target) {
try { 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); surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mOverlay.drawOn(target, surfaceCanvas); mOverlay.drawOn(target, surfaceCanvas);
mSurface.unlockCanvasAndPost(surfaceCanvas); mSurface.unlockCanvasAndPost(surfaceCanvas);
@ -77,7 +80,7 @@ public class OverlayDrawer {
mIssue514Workaround.beforeOverlayUpdateTexImage(); mIssue514Workaround.beforeOverlayUpdateTexImage();
mSurfaceTexture.updateTexImage(); mSurfaceTexture.updateTexImage();
} }
mSurfaceTexture.getTransformMatrix(mTransform); mSurfaceTexture.getTransformMatrix(mTextureDrawer.getTextureTransform());
} }
/** /**
@ -86,7 +89,7 @@ public class OverlayDrawer {
* @return the transform matrix * @return the transform matrix
*/ */
public float[] getTransform() { 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); GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
synchronized (mIssue514WorkaroundLock) { synchronized (mIssue514WorkaroundLock) {
mViewport.drawFrame(timestampUs, mTextureId, mTransform); mTextureDrawer.draw(timestampUs);
} }
} }
@ -125,9 +128,9 @@ public class OverlayDrawer {
mSurface.release(); mSurface.release();
mSurface = null; mSurface = null;
} }
if (mViewport != null) { if (mTextureDrawer != null) {
mViewport.release(); mTextureDrawer.release();
mViewport = null; mTextureDrawer = null;
} }
} }
} }

@ -26,6 +26,8 @@ public class OverlayLayout extends FrameLayout implements Overlay {
@VisibleForTesting Target currentTarget = Target.PREVIEW; @VisibleForTesting Target currentTarget = Target.PREVIEW;
private boolean mHardwareCanvasEnabled;
/** /**
* We set {@link #setWillNotDraw(boolean)} to false even if we don't draw anything. * 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 * 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; 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 * For {@link Target#PREVIEW}, this method is called by the View hierarchy. We will
* just forward the call to super. * just forward the call to super.
@ -132,7 +144,8 @@ public class OverlayLayout extends FrameLayout implements Overlay {
"canvas:", canvas.getWidth() + "x" + canvas.getHeight(), "canvas:", canvas.getWidth() + "x" + canvas.getHeight(),
"view:", getWidth() + "x" + getHeight(), "view:", getWidth() + "x" + getHeight(),
"widthScale:", widthScale, "widthScale:", widthScale,
"heightScale:", heightScale "heightScale:", heightScale,
"hardwareCanvasMode:", mHardwareCanvasEnabled
); );
canvas.scale(widthScale, heightScale); canvas.scale(widthScale, heightScale);
dispatchDraw(canvas); dispatchDraw(canvas);

@ -1,15 +1,19 @@
package com.otaliastudios.cameraview.picture; package com.otaliastudios.cameraview.picture;
import android.hardware.Camera; import android.hardware.Camera;
import android.util.Log;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.internal.utils.ExifHelper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface; 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.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -41,40 +45,63 @@ public class Full1PictureRecorder extends FullPictureRecorder {
// Stopping the preview callback is important on older APIs / emulators, // Stopping the preview callback is important on older APIs / emulators,
// or takePicture can hang and leave the camera in a bad state. // or takePicture can hang and leave the camera in a bad state.
mCamera.setPreviewCallbackWithBuffer(null); mCamera.setPreviewCallbackWithBuffer(null);
mCamera.takePicture( mEngine.getFrameManager().release();
new Camera.ShutterCallback() { try {
@Override mCamera.takePicture(
public void onShutter() { new Camera.ShutterCallback() {
LOG.i("take(): got onShutter callback."); @Override
dispatchOnShutter(true); public void onShutter() {
} LOG.i("take(): got onShutter callback.");
}, dispatchOnShutter(true);
null, }
null, },
new Camera.PictureCallback() { null,
@Override null,
public void onPictureTaken(byte[] data, final Camera camera) { new Camera.PictureCallback() {
LOG.i("take(): got picture callback."); @Override
int exifRotation; public void onPictureTaken(byte[] data, final Camera camera) {
try { LOG.i("take(): got picture callback.");
ExifInterface exif = new ExifInterface(new ByteArrayInputStream(data)); int exifRotation;
int exifOrientation = exif.getAttributeInt( try {
ExifInterface.TAG_ORIENTATION, ExifInterface exif = new ExifInterface(new ByteArrayInputStream(data));
ExifInterface.ORIENTATION_NORMAL); int exifOrientation = exif.getAttributeInt(
exifRotation = ExifHelper.getOrientation(exifOrientation); ExifInterface.TAG_ORIENTATION,
} catch (IOException e) { ExifInterface.ORIENTATION_NORMAL);
exifRotation = 0; 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 @Override

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

@ -7,9 +7,9 @@ import android.hardware.Camera;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.engine.Camera1Engine; import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.internal.utils.CropHelper; import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.internal.utils.RotationHelper; import com.otaliastudios.cameraview.internal.RotationHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler; import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;

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

@ -10,33 +10,31 @@ import android.opengl.Matrix;
import android.os.Build; import android.os.Build;
import com.otaliastudios.cameraview.PictureResult; import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.internal.egl.EglBaseSurface; import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.overlay.Overlay; import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.internal.egl.EglCore; import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.internal.egl.EglViewport; import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.internal.egl.EglWindowSurface;
import com.otaliastudios.cameraview.internal.utils.CropHelper;
import com.otaliastudios.cameraview.internal.utils.WorkerHandler;
import com.otaliastudios.cameraview.overlay.OverlayDrawer; import com.otaliastudios.cameraview.overlay.OverlayDrawer;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback; import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread; import com.otaliastudios.cameraview.preview.RendererThread;
import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.opengl.core.EglCore;
import com.otaliastudios.opengl.surface.EglSurface;
import com.otaliastudios.opengl.surface.EglWindowSurface;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import android.view.Surface; import android.view.Surface;
/** /**
* API 19. * API 19.
* Records a picture snapshots from the {@link GlCameraPreview}. It works as follows: * Records a picture snapshots from the {@link RendererCameraPreview}. It works as follows:
* *
* - We register a one time {@link RendererFrameCallback} on the preview * - We register a one time {@link RendererFrameCallback} on the preview
* - We get the textureId and the frame callback on the {@link RendererThread} * - We get the textureId and the frame callback on the {@link RendererThread}
@ -45,7 +43,7 @@ import android.view.Surface;
* - We move to another thread, and create a new EGL surface for that EGL context. * - We move to another thread, and create a new EGL surface for that EGL context.
* - We make this new surface current, and re-draw the textureId on it * - We make this new surface current, and re-draw the textureId on it
* - [Optional: fill the overlayTextureId and draw it on the same surface] * - [Optional: fill the overlayTextureId and draw it on the same surface]
* - We use glReadPixels (through {@link EglBaseSurface#saveFrameTo(Bitmap.CompressFormat)}) * - We use glReadPixels (through {@link EglSurface#toByteArray(Bitmap.CompressFormat)})
* and save to file. * and save to file.
* *
* We create a new EGL surface and redraw the frame because: * We create a new EGL surface and redraw the frame because:
@ -55,30 +53,24 @@ import android.view.Surface;
*/ */
public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder { public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
private CameraEngine mEngine; private RendererCameraPreview mPreview;
private GlCameraPreview mPreview;
private AspectRatio mOutputRatio; private AspectRatio mOutputRatio;
private Overlay mOverlay; private Overlay mOverlay;
private boolean mHasOverlay; private boolean mHasOverlay;
private OverlayDrawer mOverlayDrawer; private OverlayDrawer mOverlayDrawer;
private GlTextureDrawer mTextureDrawer;
private int mTextureId;
private float[] mTransform;
private EglViewport mViewport;
public SnapshotGlPictureRecorder( public SnapshotGlPictureRecorder(
@NonNull PictureResult.Stub stub, @NonNull PictureResult.Stub stub,
@NonNull CameraEngine engine, @Nullable PictureResultListener listener,
@NonNull GlCameraPreview preview, @NonNull RendererCameraPreview preview,
@NonNull AspectRatio outputRatio) { @NonNull AspectRatio outputRatio,
super(stub, engine); @Nullable Overlay overlay) {
mEngine = engine; super(stub, listener);
mPreview = preview; mPreview = preview;
mOutputRatio = outputRatio; mOutputRatio = outputRatio;
mOverlay = engine.getOverlay(); mOverlay = overlay;
mHasOverlay = mOverlay != null && mOverlay.drawsOn(Overlay.Target.PICTURE_SNAPSHOT); mHasOverlay = mOverlay != null && mOverlay.drawsOn(Overlay.Target.PICTURE_SNAPSHOT);
} }
@ -101,10 +93,10 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@RendererThread @RendererThread
@Override @Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture,
final float scaleX, int rotation, float scaleX, float scaleY) {
final float scaleY) {
mPreview.removeRendererFrameCallback(this); mPreview.removeRendererFrameCallback(this);
SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture, scaleX, scaleY); SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture,
rotation, scaleX, scaleY);
} }
}); });
@ -114,14 +106,10 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@RendererThread @RendererThread
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
protected void onRendererTextureCreated(int textureId) { protected void onRendererTextureCreated(int textureId) {
mTextureId = textureId; mTextureDrawer = new GlTextureDrawer(textureId);
mViewport = new EglViewport();
// Need to crop the size. // Need to crop the size.
Rect crop = CropHelper.computeCrop(mResult.size, mOutputRatio); Rect crop = CropHelper.computeCrop(mResult.size, mOutputRatio);
mResult.size = new Size(crop.width(), crop.height()); mResult.size = new Size(crop.width(), crop.height());
mTransform = new float[16];
Matrix.setIdentityM(mTransform, 0);
if (mHasOverlay) { if (mHasOverlay) {
mOverlayDrawer = new OverlayDrawer(mOverlay, mResult.size); mOverlayDrawer = new OverlayDrawer(mOverlay, mResult.size);
} }
@ -131,25 +119,24 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@RendererThread @RendererThread
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
protected void onRendererFilterChanged(@NonNull Filter filter) { protected void onRendererFilterChanged(@NonNull Filter filter) {
mViewport.setFilter(filter.copy()); mTextureDrawer.setFilter(filter.copy());
} }
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
@RendererThread @RendererThread
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
protected void onRendererFrame(@SuppressWarnings("unused") @NonNull final SurfaceTexture surfaceTexture, protected void onRendererFrame(@SuppressWarnings("unused") @NonNull final SurfaceTexture surfaceTexture,
final int rotation,
final float scaleX, final float scaleX,
final float scaleY) { final float scaleY) {
// Get egl context from the RendererThread, which is the one in which we have created // Get egl context from the RendererThread, which is the one in which we have created
// the textureId and the overlayTextureId, managed by the GlSurfaceView. // the textureId and the overlayTextureId, managed by the GlSurfaceView.
// Next operations can then be performed on different threads using this handle. // Next operations can then be performed on different threads using this handle.
final EGLContext eglContext = EGL14.eglGetCurrentContext(); final EGLContext eglContext = EGL14.eglGetCurrentContext();
// Calling this invalidates the rotation/scale logic below:
// surfaceTexture.getTransformMatrix(mTransform); // TODO activate and fix the logic.
WorkerHandler.execute(new Runnable() { WorkerHandler.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
takeFrame(surfaceTexture, scaleX, scaleY, eglContext); takeFrame(surfaceTexture, rotation, scaleX, scaleY, eglContext);
} }
}); });
@ -182,6 +169,7 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@WorkerThread @WorkerThread
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
protected void takeFrame(@NonNull SurfaceTexture surfaceTexture, protected void takeFrame(@NonNull SurfaceTexture surfaceTexture,
int rotation,
float scaleX, float scaleX,
float scaleY, float scaleY,
@NonNull EGLContext eglContext) { @NonNull EGLContext eglContext) {
@ -194,26 +182,23 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
// 1. Create an EGL surface // 1. Create an EGL surface
final EglCore core = new EglCore(eglContext, EglCore.FLAG_RECORDABLE); final EglCore core = new EglCore(eglContext, EglCore.FLAG_RECORDABLE);
final EglBaseSurface eglSurface = new EglWindowSurface(core, fakeOutputSurface); final EglSurface eglSurface = new EglWindowSurface(core, fakeOutputSurface);
eglSurface.makeCurrent(); eglSurface.makeCurrent();
final float[] transform = mTextureDrawer.getTextureTransform();
// 2. Apply scale and crop // 2. Apply preview transformations
boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR); surfaceTexture.getTransformMatrix(transform);
float realScaleX = flip ? scaleY : scaleX; float scaleTranslX = (1F - scaleX) / 2F;
float realScaleY = flip ? scaleX : scaleY; float scaleTranslY = (1F - scaleY) / 2F;
float scaleTranslX = (1F - realScaleX) / 2F; Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0);
float scaleTranslY = (1F - realScaleY) / 2F; Matrix.scaleM(transform, 0, scaleX, scaleY, 1);
Matrix.translateM(mTransform, 0, scaleTranslX, scaleTranslY, 0);
Matrix.scaleM(mTransform, 0, realScaleX, realScaleY, 1);
// 3. Apply rotation and flip // 3. Apply rotation and flip
Matrix.translateM(mTransform, 0, 0.5F, 0.5F, 0); // Go back to 0,0 // If this doesn't work, rotate "rotation" before scaling, like GlCameraPreview does.
Matrix.rotateM(mTransform, 0, -mResult.rotation, 0, 0, 1); // Rotate (not sure why we need the minus) Matrix.translateM(transform, 0, 0.5F, 0.5F, 0); // Go back to 0,0
mResult.rotation = 0; Matrix.rotateM(transform, 0, rotation + mResult.rotation, 0, 0, 1); // Rotate to OUTPUT
if (mResult.facing == Facing.FRONT) { // 5. Flip horizontally for front camera Matrix.scaleM(transform, 0, 1, -1, 1); // Vertical flip because we'll use glReadPixels
Matrix.scaleM(mTransform, 0, -1, 1, 1); Matrix.translateM(transform, 0, -0.5F, -0.5F, 0); // Go back to old position
}
Matrix.translateM(mTransform, 0, -0.5F, -0.5F, 0); // Go back to old position
// 4. Do pretty much the same for overlays // 4. Do pretty much the same for overlays
if (mHasOverlay) { if (mHasOverlay) {
@ -221,24 +206,23 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
mOverlayDrawer.draw(Overlay.Target.PICTURE_SNAPSHOT); mOverlayDrawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
// 2. Then we can apply the transformations // 2. Then we can apply the transformations
int rotation = mEngine.getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
Matrix.translateM(mOverlayDrawer.getTransform(), 0, 0.5F, 0.5F, 0); Matrix.translateM(mOverlayDrawer.getTransform(), 0, 0.5F, 0.5F, 0);
Matrix.rotateM(mOverlayDrawer.getTransform(), 0, rotation, 0, 0, 1); Matrix.rotateM(mOverlayDrawer.getTransform(), 0, mResult.rotation, 0, 0, 1);
// No need to flip the x axis for front camera, but need to flip the y axis always. Matrix.scaleM(mOverlayDrawer.getTransform(), 0, 1, -1, 1); // Vertical flip because we'll use glReadPixels
Matrix.scaleM(mOverlayDrawer.getTransform(), 0, 1, -1, 1);
Matrix.translateM(mOverlayDrawer.getTransform(), 0, -0.5F, -0.5F, 0); Matrix.translateM(mOverlayDrawer.getTransform(), 0, -0.5F, -0.5F, 0);
} }
mResult.rotation = 0;
// 5. Draw and save // 5. Draw and save
long timestampUs = surfaceTexture.getTimestamp() / 1000L; long timestampUs = surfaceTexture.getTimestamp() / 1000L;
LOG.i("takeFrame:", "timestampUs:", timestampUs); LOG.i("takeFrame:", "timestampUs:", timestampUs);
mViewport.drawFrame(timestampUs, mTextureId, mTransform); mTextureDrawer.draw(timestampUs);
if (mHasOverlay) mOverlayDrawer.render(timestampUs); if (mHasOverlay) mOverlayDrawer.render(timestampUs);
mResult.data = eglSurface.saveFrameTo(Bitmap.CompressFormat.JPEG); mResult.data = eglSurface.toByteArray(Bitmap.CompressFormat.JPEG);
// 6. Cleanup // 6. Cleanup
eglSurface.releaseEglSurface(); eglSurface.release();
mViewport.release(); mTextureDrawer.release();
fakeOutputSurface.release(); fakeOutputSurface.release();
if (mHasOverlay) mOverlayDrawer.release(); if (mHasOverlay) mOverlayDrawer.release();
core.release(); core.release();
@ -247,7 +231,6 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@Override @Override
protected void dispatchResult() { protected void dispatchResult() {
mEngine = null;
mOutputRatio = null; mOutputRatio = null;
super.dispatchResult(); super.dispatchResult();
} }

@ -18,6 +18,7 @@ import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks; import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
/** /**
@ -62,18 +63,24 @@ public abstract class CameraPreview<T extends View, Output> {
@VisibleForTesting CropCallback mCropCallback; @VisibleForTesting CropCallback mCropCallback;
private SurfaceCallback mSurfaceCallback; private SurfaceCallback mSurfaceCallback;
private T mView; private T mView;
boolean mCropping; @SuppressWarnings("WeakerAccess")
protected boolean mCropping;
// These are the surface dimensions in REF_VIEW. // These are the surface dimensions in REF_VIEW.
int mOutputSurfaceWidth; @SuppressWarnings("WeakerAccess")
int mOutputSurfaceHeight; protected int mOutputSurfaceWidth;
@SuppressWarnings("WeakerAccess")
protected int mOutputSurfaceHeight;
// These are the preview stream dimensions, in REF_VIEW. // These are the preview stream dimensions, in REF_VIEW.
int mInputStreamWidth; @SuppressWarnings("WeakerAccess")
int mInputStreamHeight; protected int mInputStreamWidth;
@SuppressWarnings("WeakerAccess")
protected int mInputStreamHeight;
// The rotation, if any, to be applied when drawing. // The rotation, if any, to be applied when drawing.
int mDrawRotation; @SuppressWarnings("WeakerAccess")
protected int mDrawRotation;
/** /**
* Creates a new preview. * Creates a new preview.
@ -88,7 +95,7 @@ public abstract class CameraPreview<T extends View, Output> {
* Sets a callback to be notified of surface events (creation, change, destruction) * Sets a callback to be notified of surface events (creation, change, destruction)
* @param callback a callback * @param callback a callback
*/ */
public final void setSurfaceCallback(@Nullable SurfaceCallback callback) { public void setSurfaceCallback(@Nullable SurfaceCallback callback) {
if (hasSurface() && mSurfaceCallback != null) { if (hasSurface() && mSurfaceCallback != null) {
mSurfaceCallback.onSurfaceDestroyed(); mSurfaceCallback.onSurfaceDestroyed();
} }
@ -124,9 +131,8 @@ public abstract class CameraPreview<T extends View, Output> {
* @return the root view * @return the root view
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
@VisibleForTesting
@NonNull @NonNull
abstract View getRootView(); public abstract View getRootView();
/** /**
* Returns the output surface object (for example a SurfaceHolder * Returns the output surface object (for example a SurfaceHolder
@ -162,7 +168,7 @@ public abstract class CameraPreview<T extends View, Output> {
* Returns the current input stream size, in view coordinates. * Returns the current input stream size, in view coordinates.
* @return the current input stream size * @return the current input stream size
*/ */
@SuppressWarnings("unused") @VisibleForTesting
@NonNull @NonNull
final Size getStreamSize() { final Size getStreamSize() {
return new Size(mInputStreamWidth, mInputStreamHeight); return new Size(mInputStreamWidth, mInputStreamHeight);
@ -326,11 +332,15 @@ public abstract class CameraPreview<T extends View, Output> {
* Sometimes we don't need this: * Sometimes we don't need this:
* - In Camera1, the buffer producer sets our Surface size and rotates it based on the value * - In Camera1, the buffer producer sets our Surface size and rotates it based on the value
* that we pass to {@link android.hardware.Camera.Parameters#setDisplayOrientation(int)}, * that we pass to {@link android.hardware.Camera.Parameters#setDisplayOrientation(int)},
* so the stream that comes in is already rotated. * so the stream that comes in is already rotated (if we apply SurfaceTexture transform).
* - In Camera2, for {@link android.view.SurfaceView} based previews, apparently it just works * - In Camera2, for {@link android.view.SurfaceView} based previews, apparently it just works
* out of the box. The producer might be doing something similar. * out of the box. The producer might be doing something similar.
* *
* But in all the other Camera2 cases, we need to apply this rotation when drawing the surface. * But in all the other Camera2 cases, we need to apply this rotation when drawing the surface.
* Seems that Camera1 can correctly rotate the stream/transform to {@link Reference#VIEW},
* while Camera2, that does not have any rotation API, will only rotate to {@link Reference#BASE}.
* That's why in Camera2 this angle is set as the offset between BASE and VIEW.
*
* @param drawRotation the rotation in degrees * @param drawRotation the rotation in degrees
*/ */
public void setDrawRotation(int drawRotation) { public void setDrawRotation(int drawRotation) {

@ -16,18 +16,13 @@ import com.otaliastudios.cameraview.filter.Filter;
* The preview has the responsibility of calling {@link Filter#setSize(int, int)} * The preview has the responsibility of calling {@link Filter#setSize(int, int)}
* whenever the preview size changes and as soon as the filter is applied. * whenever the preview size changes and as soon as the filter is applied.
*/ */
public abstract class FilterCameraPreview<T extends View, Output> extends CameraPreview<T, Output> { public interface FilterCameraPreview {
@SuppressWarnings("WeakerAccess")
public FilterCameraPreview(@NonNull Context context, @NonNull ViewGroup parent) {
super(context, parent);
}
/** /**
* Sets a new filter. * Sets a new filter.
* @param filter new filter * @param filter new filter
*/ */
public abstract void setFilter(@NonNull Filter filter); void setFilter(@NonNull Filter filter);
/** /**
* Returns the currently used filter. * Returns the currently used filter.
@ -35,5 +30,5 @@ public abstract class FilterCameraPreview<T extends View, Output> extends Camera
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
@NonNull @NonNull
public abstract Filter getCurrentFilter(); Filter getCurrentFilter();
} }

@ -14,7 +14,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.otaliastudios.cameraview.R; import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.internal.egl.EglViewport; import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.filter.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.NoFilter; import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
@ -60,13 +60,12 @@ import javax.microedition.khronos.opengles.GL10;
* which means that we can fetch the GL context that was created and is managed * which means that we can fetch the GL context that was created and is managed
* by the {@link GLSurfaceView}. * by the {@link GLSurfaceView}.
*/ */
public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceTexture> { public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture>
implements FilterCameraPreview, RendererCameraPreview {
private boolean mDispatched; private boolean mDispatched;
private final float[] mTransformMatrix = new float[16];
private int mOutputTextureId = 0;
private SurfaceTexture mInputSurfaceTexture; private SurfaceTexture mInputSurfaceTexture;
private EglViewport mOutputViewport; private GlTextureDrawer mOutputTextureDrawer;
// A synchronized set was not enough to avoid crashes, probably due to external classes // A synchronized set was not enough to avoid crashes, probably due to external classes
// removing the callback while this set is being iterated. CopyOnWriteArraySet solves this. // removing the callback while this set is being iterated. CopyOnWriteArraySet solves this.
private final Set<RendererFrameCallback> mRendererFrameCallbacks = new CopyOnWriteArraySet<>(); private final Set<RendererFrameCallback> mRendererFrameCallbacks = new CopyOnWriteArraySet<>();
@ -112,7 +111,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
@NonNull @NonNull
@Override @Override
View getRootView() { public View getRootView() {
return mRootView; return mRootView;
} }
@ -146,14 +145,15 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
if (mCurrentFilter == null) { if (mCurrentFilter == null) {
mCurrentFilter = new NoFilter(); mCurrentFilter = new NoFilter();
} }
mOutputViewport = new EglViewport(mCurrentFilter); mOutputTextureDrawer = new GlTextureDrawer();
mOutputTextureId = mOutputViewport.createTexture(); mOutputTextureDrawer.setFilter(mCurrentFilter);
mInputSurfaceTexture = new SurfaceTexture(mOutputTextureId); final int textureId = mOutputTextureDrawer.getTexture().getId();
mInputSurfaceTexture = new SurfaceTexture(textureId);
getView().queueEvent(new Runnable() { getView().queueEvent(new Runnable() {
@Override @Override
public void run() { public void run() {
for (RendererFrameCallback callback : mRendererFrameCallbacks) { for (RendererFrameCallback callback : mRendererFrameCallbacks) {
callback.onRendererTextureCreated(mOutputTextureId); callback.onRendererTextureCreated(textureId);
} }
} }
}); });
@ -176,10 +176,9 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
mInputSurfaceTexture.release(); mInputSurfaceTexture.release();
mInputSurfaceTexture = null; mInputSurfaceTexture = null;
} }
mOutputTextureId = 0; if (mOutputTextureDrawer != null) {
if (mOutputViewport != null) { mOutputTextureDrawer.release();
mOutputViewport.release(); mOutputTextureDrawer = null;
mOutputViewport = null;
} }
} }
@ -207,17 +206,17 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
// Latch the latest frame. If there isn't anything new, // Latch the latest frame. If there isn't anything new,
// we'll just re-use whatever was there before. // we'll just re-use whatever was there before.
final float[] transform = mOutputTextureDrawer.getTextureTransform();
mInputSurfaceTexture.updateTexImage(); mInputSurfaceTexture.updateTexImage();
mInputSurfaceTexture.getTransformMatrix(mTransformMatrix); mInputSurfaceTexture.getTransformMatrix(transform);
// LOG.v("onDrawFrame:", "timestamp:", mInputSurfaceTexture.getTimestamp()); // LOG.v("onDrawFrame:", "timestamp:", mInputSurfaceTexture.getTimestamp());
// For Camera2, apply the draw rotation. // For Camera2, apply the draw rotation.
// See TextureCameraPreview.setDrawRotation() for info. // See TextureCameraPreview.setDrawRotation() for info.
if (mDrawRotation != 0) { if (mDrawRotation != 0) {
Matrix.translateM(mTransformMatrix, 0, 0.5F, 0.5F, 0); Matrix.translateM(transform, 0, 0.5F, 0.5F, 0);
Matrix.rotateM(mTransformMatrix, 0, mDrawRotation, 0, 0, 1); Matrix.rotateM(transform, 0, mDrawRotation, 0, 0, 1);
Matrix.translateM(mTransformMatrix, 0, -0.5F, -0.5F, 0); Matrix.translateM(transform, 0, -0.5F, -0.5F, 0);
} }
if (isCropping()) { if (isCropping()) {
@ -228,13 +227,13 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
// of the preview (not the center one). // of the preview (not the center one).
float translX = (1F - mCropScaleX) / 2F; float translX = (1F - mCropScaleX) / 2F;
float translY = (1F - mCropScaleY) / 2F; float translY = (1F - mCropScaleY) / 2F;
Matrix.translateM(mTransformMatrix, 0, translX, translY, 0); Matrix.translateM(transform, 0, translX, translY, 0);
Matrix.scaleM(mTransformMatrix, 0, mCropScaleX, mCropScaleY, 1); Matrix.scaleM(transform, 0, mCropScaleX, mCropScaleY, 1);
} }
mOutputViewport.drawFrame(mInputSurfaceTexture.getTimestamp() / 1000L,
mOutputTextureId, mTransformMatrix); mOutputTextureDrawer.draw(mInputSurfaceTexture.getTimestamp() / 1000L);
for (RendererFrameCallback callback : mRendererFrameCallbacks) { for (RendererFrameCallback callback : mRendererFrameCallbacks) {
callback.onRendererFrame(mInputSurfaceTexture, mCropScaleX, mCropScaleY); callback.onRendererFrame(mInputSurfaceTexture, mDrawRotation, mCropScaleX, mCropScaleY);
} }
} }
} }
@ -291,27 +290,22 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
if (callback != null) callback.onCrop(); if (callback != null) callback.onCrop();
} }
/** @Override
* Method specific to the GL preview. Adds a {@link RendererFrameCallback}
* to receive renderer frame events.
* @param callback a callback
*/
public void addRendererFrameCallback(@NonNull final RendererFrameCallback callback) { public void addRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
getView().queueEvent(new Runnable() { getView().queueEvent(new Runnable() {
@Override @Override
public void run() { public void run() {
mRendererFrameCallbacks.add(callback); mRendererFrameCallbacks.add(callback);
if (mOutputTextureId != 0) callback.onRendererTextureCreated(mOutputTextureId); if (mOutputTextureDrawer != null) {
int textureId = mOutputTextureDrawer.getTexture().getId();
callback.onRendererTextureCreated(textureId);
}
callback.onRendererFilterChanged(mCurrentFilter); callback.onRendererFilterChanged(mCurrentFilter);
} }
}); });
} }
/** @Override
* Method specific to the GL preview. Removes a {@link RendererFrameCallback}
* that was previously added to receive renderer frame events.
* @param callback a callback
*/
public void removeRendererFrameCallback(@NonNull final RendererFrameCallback callback) { public void removeRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
mRendererFrameCallbacks.remove(callback); mRendererFrameCallbacks.remove(callback);
} }
@ -322,7 +316,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
protected int getTextureId() { protected int getTextureId() {
return mOutputTextureId; return mOutputTextureDrawer != null ? mOutputTextureDrawer.getTexture().getId() : -1;
} }
/** /**
@ -354,8 +348,8 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
getView().queueEvent(new Runnable() { getView().queueEvent(new Runnable() {
@Override @Override
public void run() { public void run() {
if (mOutputViewport != null) { if (mOutputTextureDrawer != null) {
mOutputViewport.setFilter(filter); mOutputTextureDrawer.setFilter(filter);
} }
for (RendererFrameCallback callback : mRendererFrameCallbacks) { for (RendererFrameCallback callback : mRendererFrameCallbacks) {
callback.onRendererFilterChanged(filter); callback.onRendererFilterChanged(filter);

@ -0,0 +1,23 @@
package com.otaliastudios.cameraview.preview;
import androidx.annotation.NonNull;
/**
* Base interface for previews that support renderer frame callbacks,
* see {@link RendererFrameCallback}.
*/
public interface RendererCameraPreview {
/**
* Adds a {@link RendererFrameCallback} to receive renderer frame events.
* @param callback a callback
*/
void addRendererFrameCallback(@NonNull final RendererFrameCallback callback);
/**
* Removes a {@link RendererFrameCallback} that was previously added to receive renderer
* frame events.
* @param callback a callback
*/
void removeRendererFrameCallback(@NonNull final RendererFrameCallback callback);
}

@ -24,17 +24,17 @@ public interface RendererFrameCallback {
* Called on the renderer thread after each frame was drawn. * Called on the renderer thread after each frame was drawn.
* You are not supposed to hold for too long onto this thread, because * You are not supposed to hold for too long onto this thread, because
* well, it is the rendering thread. * well, it is the rendering thread.
*
* @param surfaceTexture the texture to get transformation * @param surfaceTexture the texture to get transformation
* @param rotation the rotation (to reach REF_VIEW)
* @param scaleX the scaleX (in REF_VIEW) value * @param scaleX the scaleX (in REF_VIEW) value
* @param scaleY the scaleY (in REF_VIEW) value * @param scaleY the scaleY (in REF_VIEW) value
*/ */
@RendererThread @RendererThread
void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, float scaleX, float scaleY); void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, int rotation, float scaleX, float scaleY);
/** /**
* Called when the renderer filter changes. This is guaranteed to be called at least once * Called when the renderer filter changes. This is guaranteed to be called at least once
* before the first {@link #onRendererFrame(SurfaceTexture, float, float)}. * before the first {@link #onRendererFrame(SurfaceTexture, int, float, float)}.
* *
* @param filter the new filter * @param filter the new filter
*/ */

@ -46,6 +46,7 @@ public class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHold
public void surfaceCreated(SurfaceHolder holder) { public void surfaceCreated(SurfaceHolder holder) {
// This is too early to call anything. // This is too early to call anything.
// surfaceChanged is guaranteed to be called after, with exact dimensions. // surfaceChanged is guaranteed to be called after, with exact dimensions.
LOG.i("callback: surfaceCreated.");
} }
@Override @Override
@ -64,7 +65,7 @@ public class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHold
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
LOG.i("callback:", "surfaceDestroyed"); LOG.i("callback: surfaceDestroyed");
dispatchOnSurfaceDestroyed(); dispatchOnSurfaceDestroyed();
mDispatched = false; mDispatched = false;
} }
@ -75,7 +76,7 @@ public class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHold
@NonNull @NonNull
@Override @Override
View getRootView() { public View getRootView() {
return mRootView; return mRootView;
} }

@ -68,7 +68,7 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
@NonNull @NonNull
@Override @Override
View getRootView() { public View getRootView() {
return mRootView; return mRootView;
} }

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

Loading…
Cancel
Save