Compare commits

..

54 Commits

Author SHA1 Message Date
DeweyReed e26ac950be
Fix the setSize typo (#1198) 2 years ago
Dmitry Naymushin ae3f3da0e1
A couple of fixes and docs improvements (#1184) 2 years ago
Steve Xu 82ea49f6e2
#1088: add Pixel 4a into FpsRangeValidator (#1164) 3 years ago
Mattia Iavarone 0057981408
Release v2.7.2 (#1140) 3 years ago
Mattia Iavarone 399844321b
Fix orientation when activity handles rotations (#1117) 3 years ago
Mattia Iavarone c2e02923de Fix sonatype build 3 years ago
Mattia Iavarone 994b1dd434
Release 2.7.1 (#1111) 3 years ago
Jan Šmuk ceb0eca394
Add Pixel 4a into FpsRangeValidator (#1089) 4 years ago
Ryan Loebs bde88a946e
Add option to capture hardware accelerated overlays (#1066) 4 years ago
Ryan Loebs f608ec3918
Add flag to disable automatic gesture handling (#1068) 4 years ago
Mattia Iavarone a219073468
Release v2.7.0 (#1040) 4 years ago
Ezequiel Adrián d89491195c
Implement Shutter callback. Add GestureAction.TAKE_PICTURE_SNAPSHOT. (#1030) 4 years ago
Michael 5d10277d7b
add try catch to catch exceptions when takePicture fails (#1024) 4 years ago
Mattia Iavarone 0f6ab20c3e
Make filterFrameRateRanges protected (#1026) 4 years ago
Billy Sutomo a9fcd6506e
Update frame-processing.md (#941) 4 years ago
Philippe Banwarth 6aa2d3a489
Log errors when writing the image to a file (#960) 4 years ago
Mattia Iavarone 40ace54c19
Fix preview on Pixel devices, fix deadlock with surface preview (#1020) 4 years ago
Mattia Iavarone 2429cc114b
Add CameraEngine.getRepeatingRequestDefaultTemplate (#1004) 4 years ago
Mattia Iavarone 0001ab7a54
Rewrite orchestrator (#992) 4 years ago
Mattia Iavarone e3fcef286f
Release v2.6.4 (#954) 4 years ago
Mattia Iavarone 8207e67679
Some bug fixes (#953) 4 years ago
thijsonline daf7a0bf44
Fix for #927 OrientationHelper.enable() called twice (#928) 4 years ago
Steve 170d87ed39
Fix references on overlay documentation (#931) 4 years ago
Mattia Iavarone a7cf726636 Fix isMinifyEnabled 4 years ago
Mattia Iavarone 610f511a76
Release v2.6.3 (#912) 4 years ago
Mattia Iavarone a7c18324a5
Build tools update (#911) 4 years ago
Alexander 66c37373bd
Wrapped some crashes which happened on start camera and which connect with params. (#897) 4 years ago
Alexander 8455c23fb1
Wrapped some crashes which connect with empty camera params (#851) 4 years ago
jeffreyfjohnson b5921ec6bf
Restart Camera1 Frame Processing After Taking a Picture - Fixes Issue 824 (#877) 4 years ago
Ryan McClarnon 4364ec7c3a
Add capability to set audio codec to encode video files with. (#861) 5 years ago
Mattia Iavarone edbade24a9
Preview interface changes (#816) 5 years ago
Alexander 5530c011fa
Add ability to clear lifecycleObserver (#798) 5 years ago
Mattia Iavarone 778cdedc00
Release v2.6.2 (#794) 5 years ago
dependabot[bot] fa26b2a654
Bump nokogiri from 1.10.4 to 1.10.8 in /docs (#786) 5 years ago
Mattia Iavarone 124aee6908 Make LogAction log to info 5 years ago
Alexander 218b27319d
Add setter for flag enabled/disabled auto check permissions (#775) 5 years ago
Alexander 858495afde
Make CameraLogger ThreadSafe (#779) 5 years ago
Mattia Iavarone aa95d19902 Ads 5 years ago
Mattia Iavarone 968d181af4
GL cleanup (#766) 5 years ago
dependabot[bot] 7f23c309ab Bump rubyzip from 1.2.2 to 2.0.0 in /docs (#759) 5 years ago
hualong-shen 24b02caa28 Set accurate fps if possible (#754) 5 years ago
Mattia Iavarone e207e452bf Website mobile improvements 5 years ago
Mattia Iavarone 83bb45108a
Frame rotation improvements (#745) 5 years ago
Mattia Iavarone 1bb15dc595
Update bug_report.md 5 years ago
Mattia Iavarone 949ed22ea6
Release v2.6.1 (#744) 5 years ago
Mattia Iavarone 141902c6cb
Metering improvements (#741) 5 years ago
Sewar 1a88cd09f4 Add support to record video to FileDescriptor. (#732) 5 years ago
Mattia Iavarone e6ec1a15ac Fix page.html 5 years ago
Mattia Iavarone c0dab05063 Use Jekyll collections 5 years ago
Mattia Iavarone ef9a68ec6a Website improvements 5 years ago
Mattia Iavarone adf54cb029 Fix site header (3) 5 years ago
Mattia Iavarone 0f14fbd4e8 Fix site header (2) 5 years ago
Mattia Iavarone afd02f251b Fix site header 5 years ago
Mattia Iavarone 3c4d01c22f
Improve website (#734) 5 years ago
  1. 5
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 2
      .github/ISSUE_TEMPLATE/question.md
  3. 12
      .github/pull_request_template.md
  4. 52
      .github/workflows/build.yml
  5. 21
      .github/workflows/deploy.yml
  6. 2
      .github/workflows/emulator_script.sh
  7. 25
      .github/workflows/snapshot.yml
  8. 23
      .run/runAndroidTests.run.xml
  9. 23
      .run/runUnitTests.run.xml
  10. 2
      LICENSE
  11. 5
      README.md
  12. 29
      build.gradle
  13. 30
      build.gradle.kts
  14. 270
      cameraview/build.gradle
  15. 137
      cameraview/build.gradle.kts
  16. 36
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java
  17. 8
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  18. 48
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  19. 76
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java
  20. 17
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  21. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java
  22. 24
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java
  23. 32
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java
  24. 20
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/ByteBufferFrameManagerTest.java
  25. 17
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/frame/FrameManagerTest.java
  26. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CamcorderProfilesTest.java
  27. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/CropHelperTest.java
  28. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/OrientationHelperTest.java
  29. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/RotationHelperTest.java
  30. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/WorkerHandlerTest.java
  31. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
  32. 36
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java
  33. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
  34. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
  35. 3
      cameraview/src/main/AndroidManifest.xml
  36. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java
  37. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  38. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java
  39. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  40. 7
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java
  41. 205
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  42. 38
      cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java
  43. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/AudioCodec.java
  44. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java
  45. 116
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  46. 123
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  47. 51
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java
  48. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  49. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/LogAction.java
  50. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java
  51. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera1Options.java
  52. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/options/Camera2Options.java
  53. 250
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java
  54. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java
  55. 103
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java
  56. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java
  57. 152
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java
  58. 1
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/SimpleFilter.java
  59. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java
  60. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java
  61. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java
  62. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java
  63. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java
  64. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java
  65. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java
  66. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java
  67. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java
  68. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java
  69. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java
  70. 16
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java
  71. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java
  72. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java
  73. 20
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java
  74. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/frame/ByteBufferFrameManager.java
  75. 50
      cameraview/src/main/java/com/otaliastudios/cameraview/frame/Frame.java
  76. 31
      cameraview/src/main/java/com/otaliastudios/cameraview/frame/FrameManager.java
  77. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java
  78. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/CamcorderProfiles.java
  79. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/CropHelper.java
  80. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java
  81. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/ExifHelper.java
  82. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/FpsRangeValidator.java
  83. 97
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlTextureDrawer.java
  84. 96
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java
  85. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/Issue514Workaround.java
  86. 53
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/OrientationHelper.java
  87. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/Pool.java
  88. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/RotationHelper.java
  89. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/WorkerHandler.java
  90. 244
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java
  91. 364
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglCore.java
  92. 103
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java
  93. 80
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglWindowSurface.java
  94. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/Overlay.java
  95. 35
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayDrawer.java
  96. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java
  97. 101
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java
  98. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java
  99. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot1PictureRecorder.java
  100. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java
  101. Some files were not shown because too many files have changed in this diff Show More

@ -13,11 +13,12 @@ Please add a clear description of what the bug is, **and** fill the list below.
- Camera engine used: *camera1/camera2/both* - 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
@ -38,7 +39,7 @@ Part of the XML layout with the CameraView declaration, so we can read its attri
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
### Logs ### Logs
Use `CameraLogger.setLogLevel(LEVEL_VERBOSE)` to see all logs into LogCat. Use `CameraLogger.setLogLevel(LEVEL_INFO)` to see all logs into LogCat.
Use `CameraLogger.registerLogger()` to export to file or crash reporting service. Use `CameraLogger.registerLogger()` to export to file or crash reporting service.

@ -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: 6031357
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.0' api 'com.otaliastudios:cameraview:2.7.2'
``` ```
- Fast & reliable - Fast & reliable
@ -129,6 +129,7 @@ Using CameraView is extremely simple:
app:cameraEngine="camera1|camera2" app:cameraEngine="camera1|camera2"
app:cameraPreview="glSurface|surface|texture" app:cameraPreview="glSurface|surface|texture"
app:cameraPreviewFrameRate="@integer/preview_frame_rate" app:cameraPreviewFrameRate="@integer/preview_frame_rate"
app:cameraPreviewFrameRateExact="false|true"
app:cameraFacing="back|front" app:cameraFacing="back|front"
app:cameraHdr="on|off" app:cameraHdr="on|off"
app:cameraFlash="on|auto|torch|off" app:cameraFlash="on|auto|torch|off"

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

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

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

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

@ -1,32 +1,13 @@
package com.otaliastudios.cameraview; 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);
@ -177,6 +180,7 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getFrameProcessingMaxWidth(), 0); assertEquals(cameraView.getFrameProcessingMaxWidth(), 0);
assertEquals(cameraView.getFrameProcessingMaxHeight(), 0); assertEquals(cameraView.getFrameProcessingMaxHeight(), 0);
assertEquals(cameraView.getFrameProcessingFormat(), 0); assertEquals(cameraView.getFrameProcessingFormat(), 0);
assertFalse(cameraView.getPreviewFrameRateExact());
// Self managed // Self managed
GestureParser gestures = new GestureParser(empty); GestureParser gestures = new GestureParser(empty);
@ -213,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);
@ -772,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);
@ -833,6 +861,14 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getPreviewFrameRate(), 60, 0); assertEquals(cameraView.getPreviewFrameRate(), 60, 0);
} }
@Test
public void testPreviewFrameRateExact() {
cameraView.setPreviewFrameRateExact(true);
assertTrue(cameraView.getPreviewFrameRateExact());
cameraView.setPreviewFrameRateExact(false);
assertFalse(cameraView.getPreviewFrameRateExact());
}
@Test @Test
public void testSnapshotMaxSize() { public void testSnapshotMaxSize() {
cameraView.setSnapshotMaxWidth(500); cameraView.setSnapshotMaxWidth(500);
@ -878,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
@ -1000,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);
@ -1024,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;
@ -16,6 +17,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mockito; import org.mockito.Mockito;
import java.io.File; import java.io.File;
import java.io.FileDescriptor;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -27,11 +29,12 @@ public class VideoResultTest extends BaseTest {
private VideoResult.Stub stub = new VideoResult.Stub(); private VideoResult.Stub stub = new VideoResult.Stub();
@Test @Test
public void testResult() { public void testResultWithFile() {
File file = Mockito.mock(File.class); File file = Mockito.mock(File.class);
int rotation = 90; int rotation = 90;
Size size = new Size(20, 120); Size size = new Size(20, 120);
VideoCodec codec = VideoCodec.H_263; VideoCodec videoCodec = VideoCodec.H_263;
AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
Location location = Mockito.mock(Location.class); Location location = Mockito.mock(Location.class);
boolean isSnapshot = true; boolean isSnapshot = true;
int maxDuration = 1234; int maxDuration = 1234;
@ -46,7 +49,8 @@ public class VideoResultTest extends BaseTest {
stub.file = file; stub.file = file;
stub.rotation = rotation; stub.rotation = rotation;
stub.size = size; stub.size = size;
stub.videoCodec = codec; stub.videoCodec = videoCodec;
stub.audioCodec = audioCodec;
stub.location = location; stub.location = location;
stub.isSnapshot = isSnapshot; stub.isSnapshot = isSnapshot;
stub.maxDuration = maxDuration; stub.maxDuration = maxDuration;
@ -62,7 +66,8 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getFile(), file); assertEquals(result.getFile(), file);
assertEquals(result.getRotation(), rotation); assertEquals(result.getRotation(), rotation);
assertEquals(result.getSize(), size); assertEquals(result.getSize(), size);
assertEquals(result.getVideoCodec(), codec); assertEquals(result.getVideoCodec(), videoCodec);
assertEquals(result.getAudioCodec(), audioCodec);
assertEquals(result.getLocation(), location); assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot); assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize); assertEquals(result.getMaxSize(), maxFileSize);
@ -73,6 +78,69 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getAudioBitRate(), audioBitRate); assertEquals(result.getAudioBitRate(), audioBitRate);
assertEquals(result.getAudio(), audio); assertEquals(result.getAudio(), audio);
assertEquals(result.getFacing(), facing); assertEquals(result.getFacing(), facing);
}
@Test
public void testResultWithFileDescriptor() {
FileDescriptor fileDescriptor = FileDescriptor.in;
int rotation = 90;
Size size = new Size(20, 120);
VideoCodec videoCodec = VideoCodec.H_263;
AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
Location location = Mockito.mock(Location.class);
boolean isSnapshot = true;
int maxDuration = 1234;
long maxFileSize = 500000;
int reason = VideoResult.REASON_MAX_DURATION_REACHED;
int videoFrameRate = 30;
int videoBitRate = 300000;
int audioBitRate = 30000;
Audio audio = Audio.ON;
Facing facing = Facing.FRONT;
stub.fileDescriptor = fileDescriptor;
stub.rotation = rotation;
stub.size = size;
stub.videoCodec = videoCodec;
stub.audioCodec = audioCodec;
stub.location = location;
stub.isSnapshot = isSnapshot;
stub.maxDuration = maxDuration;
stub.maxSize = maxFileSize;
stub.endReason = reason;
stub.videoFrameRate = videoFrameRate;
stub.videoBitRate = videoBitRate;
stub.audioBitRate = audioBitRate;
stub.audio = audio;
stub.facing = facing;
VideoResult result = new VideoResult(stub);
assertEquals(result.getFileDescriptor(), fileDescriptor);
assertEquals(result.getRotation(), rotation);
assertEquals(result.getSize(), size);
assertEquals(result.getVideoCodec(), videoCodec);
assertEquals(result.getAudioCodec(), audioCodec);
assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize);
assertEquals(result.getMaxDuration(), maxDuration);
assertEquals(result.getTerminationReason(), reason);
assertEquals(result.getVideoFrameRate(), videoFrameRate);
assertEquals(result.getVideoBitRate(), videoBitRate);
assertEquals(result.getAudioBitRate(), audioBitRate);
assertEquals(result.getAudio(), audio);
assertEquals(result.getFacing(), facing);
}
@Test(expected = RuntimeException.class)
public void testResultWithNoFile() {
VideoResult result = new VideoResult(stub);
result.getFile();
}
@Test(expected = RuntimeException.class)
public void testResultWithNoFileDescriptor() {
VideoResult result = new VideoResult(stub);
result.getFileDescriptor();
} }
} }

@ -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);

@ -7,6 +7,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.engine.offset.Angles;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import org.junit.After; import org.junit.After;
@ -26,6 +27,7 @@ import static org.mockito.Mockito.verify;
@SmallTest @SmallTest
public class ByteBufferFrameManagerTest extends BaseTest { public class ByteBufferFrameManagerTest extends BaseTest {
private final Angles angles = new Angles();
private ByteBufferFrameManager.BufferCallback callback; private ByteBufferFrameManager.BufferCallback callback;
@Before @Before
@ -41,22 +43,22 @@ public class ByteBufferFrameManagerTest extends BaseTest {
@Test @Test
public void testAllocate() { public void testAllocate() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback); ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50)); manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
verify(callback, times(1)).onBufferAvailable(any(byte[].class)); verify(callback, times(1)).onBufferAvailable(any(byte[].class));
reset(callback); reset(callback);
manager = new ByteBufferFrameManager(5, callback); manager = new ByteBufferFrameManager(5, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50)); manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
verify(callback, times(5)).onBufferAvailable(any(byte[].class)); verify(callback, times(5)).onBufferAvailable(any(byte[].class));
} }
@Test @Test
public void testOnFrameReleased_alreadyFull() { public void testOnFrameReleased_alreadyFull() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback); ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50)); manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes(); int length = manager.getFrameBytes();
Frame frame1 = manager.getFrame(new byte[length], 0, 0); Frame frame1 = manager.getFrame(new byte[length], 0);
assertNotNull(frame1); assertNotNull(frame1);
// Since frame1 is already taken and poolSize = 1, getFrame() would return null. // Since frame1 is already taken and poolSize = 1, getFrame() would return null.
// To create a new frame, freeze the first one. // To create a new frame, freeze the first one.
@ -73,12 +75,12 @@ public class ByteBufferFrameManagerTest extends BaseTest {
@Test @Test
public void testOnFrameReleased_sameLength() { public void testOnFrameReleased_sameLength() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback); ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50)); manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes(); int length = manager.getFrameBytes();
// A camera preview frame comes. Request a frame. // A camera preview frame comes. Request a frame.
byte[] picture = new byte[length]; byte[] picture = new byte[length];
Frame frame = manager.getFrame(picture, 0, 0); Frame frame = manager.getFrame(picture, 0);
assertNotNull(frame); assertNotNull(frame);
// Release the frame and ensure that onBufferAvailable is called. // Release the frame and ensure that onBufferAvailable is called.
@ -90,16 +92,16 @@ public class ByteBufferFrameManagerTest extends BaseTest {
@Test @Test
public void testOnFrameReleased_differentLength() { public void testOnFrameReleased_differentLength() {
ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback); ByteBufferFrameManager manager = new ByteBufferFrameManager(1, callback);
manager.setUp(ImageFormat.NV21, new Size(50, 50)); manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
int length = manager.getFrameBytes(); int length = manager.getFrameBytes();
// A camera preview frame comes. Request a frame. // A camera preview frame comes. Request a frame.
byte[] picture = new byte[length]; byte[] picture = new byte[length];
Frame frame = manager.getFrame(picture, 0, 0); Frame frame = manager.getFrame(picture, 0);
assertNotNull(frame); assertNotNull(frame);
// Don't release the frame. Change the allocation size. // Don't release the frame. Change the allocation size.
manager.setUp(ImageFormat.NV16, new Size(15, 15)); manager.setUp(ImageFormat.NV16, new Size(15, 15), angles);
// Now release the old frame and ensure that onBufferAvailable is NOT called, // Now release the old frame and ensure that onBufferAvailable is NOT called,
// because the released data has wrong length. // because the released data has wrong length.

@ -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.engine.offset.Angles;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import org.junit.Test; import org.junit.Test;
@ -18,14 +19,14 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
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.verify; import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class FrameManagerTest extends BaseTest { public class FrameManagerTest extends BaseTest {
private final Angles angles = new Angles();
@Test @Test
public void testFrameRecycling() { public void testFrameRecycling() {
// A 1-pool manager will always recycle the same frame. // A 1-pool manager will always recycle the same frame.
@ -39,12 +40,12 @@ public class FrameManagerTest extends BaseTest {
return data; return data;
} }
}; };
manager.setUp(ImageFormat.NV21, new Size(50, 50)); manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
Frame first = manager.getFrame("foo", 0, 0); Frame first = manager.getFrame("foo", 0);
assertNotNull(first); assertNotNull(first);
first.release(); first.release();
Frame second = manager.getFrame("bar", 0, 0); Frame second = manager.getFrame("bar", 0);
assertNotNull(second); assertNotNull(second);
second.release(); second.release();
assertEquals(first, second); assertEquals(first, second);
@ -62,11 +63,11 @@ public class FrameManagerTest extends BaseTest {
return data; return data;
} }
}; };
manager.setUp(ImageFormat.NV21, new Size(50, 50)); manager.setUp(ImageFormat.NV21, new Size(50, 50), angles);
Frame first = manager.getFrame("foo", 0, 0); Frame first = manager.getFrame("foo", 0);
assertNotNull(first); assertNotNull(first);
Frame second = manager.getFrame("bar", 0, 0); Frame second = manager.getFrame("bar", 0);
assertNull(second); assertNull(second);
} }
} }

@ -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;
} }
} }
@ -157,7 +158,7 @@ public class CameraUtils {
@NonNull final BitmapCallback callback) { @NonNull final BitmapCallback callback) {
decodeBitmap(source, Integer.MAX_VALUE, Integer.MAX_VALUE, callback); decodeBitmap(source, Integer.MAX_VALUE, Integer.MAX_VALUE, callback);
} }
/** /**
* Decodes an input byte array and outputs a Bitmap that is ready to be displayed. * Decodes an input byte array and outputs a Bitmap that is ready to be displayed.
* The difference with {@link android.graphics.BitmapFactory#decodeByteArray(byte[], int, int)} * The difference with {@link android.graphics.BitmapFactory#decodeByteArray(byte[], int, int)}

@ -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;
@ -86,6 +87,7 @@ import com.otaliastudios.cameraview.size.SizeSelectorParser;
import com.otaliastudios.cameraview.size.SizeSelectors; import com.otaliastudios.cameraview.size.SizeSelectors;
import java.io.File; import java.io.File;
import java.io.FileDescriptor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -131,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;
@ -205,6 +208,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
int videoBitRate = a.getInteger(R.styleable.CameraView_cameraVideoBitRate, 0); int videoBitRate = a.getInteger(R.styleable.CameraView_cameraVideoBitRate, 0);
int audioBitRate = a.getInteger(R.styleable.CameraView_cameraAudioBitRate, 0); int audioBitRate = a.getInteger(R.styleable.CameraView_cameraAudioBitRate, 0);
float videoFrameRate = a.getFloat(R.styleable.CameraView_cameraPreviewFrameRate, 0); float videoFrameRate = a.getFloat(R.styleable.CameraView_cameraPreviewFrameRate, 0);
boolean videoFrameRateExact = a.getBoolean(R.styleable.CameraView_cameraPreviewFrameRateExact, false);
long autoFocusResetDelay = (long) a.getInteger( long autoFocusResetDelay = (long) a.getInteger(
R.styleable.CameraView_cameraAutoFocusResetDelay, R.styleable.CameraView_cameraAutoFocusResetDelay,
(int) DEFAULT_AUTOFOCUS_RESET_DELAY_MILLIS); (int) DEFAULT_AUTOFOCUS_RESET_DELAY_MILLIS);
@ -223,6 +227,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
int frameExecutors = a.getInteger(R.styleable.CameraView_cameraFrameProcessingExecutors, 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);
@ -256,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().
@ -266,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);
@ -276,6 +284,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setVideoMaxDuration(videoMaxDuration); setVideoMaxDuration(videoMaxDuration);
setVideoBitRate(videoBitRate); setVideoBitRate(videoBitRate);
setAutoFocusResetDelay(autoFocusResetDelay); setAutoFocusResetDelay(autoFocusResetDelay);
setPreviewFrameRateExact(videoFrameRateExact);
setPreviewFrameRate(videoFrameRate); setPreviewFrameRate(videoFrameRate);
setSnapshotMaxWidth(snapshotMaxWidth); setSnapshotMaxWidth(snapshotMaxWidth);
setSnapshotMaxHeight(snapshotMaxHeight); setSnapshotMaxHeight(snapshotMaxHeight);
@ -389,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();
} }
@ -602,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);
@ -630,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")
@ -666,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;
@ -723,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
@ -741,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;
}
} }
/** /**
@ -828,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();
} }
@ -881,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) {
@ -917,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) {
@ -989,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());
@ -998,6 +1045,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setVideoBitRate(oldEngine.getVideoBitRate()); setVideoBitRate(oldEngine.getVideoBitRate());
setAutoFocusResetDelay(oldEngine.getAutoFocusResetDelay()); setAutoFocusResetDelay(oldEngine.getAutoFocusResetDelay());
setPreviewFrameRate(oldEngine.getPreviewFrameRate()); setPreviewFrameRate(oldEngine.getPreviewFrameRate());
setPreviewFrameRateExact(oldEngine.getPreviewFrameRateExact());
setSnapshotMaxWidth(oldEngine.getSnapshotMaxWidth()); setSnapshotMaxWidth(oldEngine.getSnapshotMaxWidth());
setSnapshotMaxHeight(oldEngine.getSnapshotMaxHeight()); setSnapshotMaxHeight(oldEngine.getSnapshotMaxHeight());
setFrameProcessingMaxWidth(oldEngine.getFrameProcessingMaxWidth()); setFrameProcessingMaxWidth(oldEngine.getFrameProcessingMaxWidth());
@ -1542,6 +1590,38 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.getVideoBitRate(); return mCameraEngine.getVideoBitRate();
} }
/**
* A flag to control the behavior when calling {@link #setPreviewFrameRate(float)}.
*
* If the value is set to true, {@link #setPreviewFrameRate(float)} will choose the preview
* frame range as close to the desired new frame rate as possible. Which mean it may choose a
* narrow range around the desired frame rate. Note: This option will give you as exact fps as
* you want but the sensor will have less freedom when adapting the exposure to the environment,
* which may lead to dark preview.
*
* If the value is set to false, {@link #setPreviewFrameRate(float)} will choose as broad range
* as it can.
*
* @param videoFrameRateExact whether want a more exact preview frame range
*
* @see #setPreviewFrameRate(float)
*/
public void setPreviewFrameRateExact(boolean videoFrameRateExact) {
mCameraEngine.setPreviewFrameRateExact(videoFrameRateExact);
}
/**
* Returns whether we want to set preview fps as exact as we set through
* {@link #setPreviewFrameRate(float)}.
*
* @see #setPreviewFrameRateExact(boolean)
* @see #setPreviewFrameRate(float)
* @return current option
*/
public boolean getPreviewFrameRateExact() {
return mCameraEngine.getPreviewFrameRateExact();
}
/** /**
* Sets the preview frame rate in frames per second. * Sets the preview frame rate in frames per second.
* This rate will be used, for example, by the frame processor and in video * This rate will be used, for example, by the frame processor and in video
@ -1585,6 +1665,30 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.getAudioBitRate(); 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.
@ -1646,8 +1750,28 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* @param file a file where the video will be saved * @param file a file where the video will be saved
*/ */
public void takeVideo(@NonNull File file) { public void takeVideo(@NonNull File file) {
takeVideo(file, null);
}
/**
* Starts recording a video. Video will be written to the given file,
* so callers should ensure they have appropriate permissions to write to the file.
*
* @param fileDescriptor a file descriptor where the video will be saved
*/
public void takeVideo(@NonNull FileDescriptor fileDescriptor) {
takeVideo(null, fileDescriptor);
}
private void takeVideo(@Nullable File file, @Nullable FileDescriptor fileDescriptor) {
VideoResult.Stub stub = new VideoResult.Stub(); VideoResult.Stub stub = new VideoResult.Stub();
mCameraEngine.takeVideo(stub, file); if (file != null) {
mCameraEngine.takeVideo(stub, file, null);
} else if (fileDescriptor != null) {
mCameraEngine.takeVideo(stub, null, fileDescriptor);
} else {
throw new IllegalStateException("file and fileDescriptor are both null.");
}
mUiHandler.post(new Runnable() { mUiHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -1686,9 +1810,27 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* *
* @param file a file where the video will be saved * @param file a file where the video will be saved
* @param durationMillis recording max duration * @param durationMillis recording max duration
*
*/ */
public void takeVideo(@NonNull File file, int durationMillis) { public void takeVideo(@NonNull File file, int durationMillis) {
takeVideo(file, null, durationMillis);
}
/**
* Starts recording a video. Video will be written to the given file,
* so callers should ensure they have appropriate permissions to write to the file.
* Recording will be automatically stopped after the given duration, overriding
* temporarily any duration limit set by {@link #setVideoMaxDuration(int)}.
*
* @param fileDescriptor a file descriptor where the video will be saved
* @param durationMillis recording max duration
*/
@SuppressWarnings("unused")
public void takeVideo(@NonNull FileDescriptor fileDescriptor, int durationMillis) {
takeVideo(null, fileDescriptor, durationMillis);
}
private void takeVideo(@Nullable File file, @Nullable FileDescriptor fileDescriptor,
int durationMillis) {
final int old = getVideoMaxDuration(); final int old = getVideoMaxDuration();
addCameraListener(new CameraListener() { addCameraListener(new CameraListener() {
@Override @Override
@ -1707,7 +1849,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
} }
}); });
setVideoMaxDuration(durationMillis); setVideoMaxDuration(durationMillis);
takeVideo(file); takeVideo(file, fileDescriptor);
} }
/** /**
@ -2017,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
@ -2097,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
@ -2201,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,15 +3,17 @@ package com.otaliastudios.cameraview;
import android.location.Location; import android.location.Location;
import com.otaliastudios.cameraview.controls.Audio; import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing; import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.VideoCodec; import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import java.io.File;
import java.io.FileDescriptor;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.File;
/** /**
* Wraps the result of a video recording started by {@link CameraView#takeVideo(File)}. * Wraps the result of a video recording started by {@link CameraView#takeVideo(File)}.
*/ */
@ -30,8 +32,10 @@ public class VideoResult {
public int rotation; public int rotation;
public Size size; public Size size;
public File file; public File file;
public FileDescriptor fileDescriptor;
public Facing facing; public Facing facing;
public VideoCodec videoCodec; public VideoCodec videoCodec;
public AudioCodec audioCodec;
public Audio audio; public Audio audio;
public long maxSize; public long maxSize;
public int maxDuration; public int maxDuration;
@ -55,8 +59,10 @@ public class VideoResult {
private final int rotation; private final int rotation;
private final Size size; private final Size size;
private final File file; private final File file;
private final FileDescriptor fileDescriptor;
private final Facing facing; private final Facing facing;
private final VideoCodec videoCodec; private final VideoCodec videoCodec;
private final AudioCodec audioCodec;
private final Audio audio; private final Audio audio;
private final long maxSize; private final long maxSize;
private final int maxDuration; private final int maxDuration;
@ -71,8 +77,10 @@ public class VideoResult {
rotation = builder.rotation; rotation = builder.rotation;
size = builder.size; size = builder.size;
file = builder.file; file = builder.file;
fileDescriptor = builder.fileDescriptor;
facing = builder.facing; facing = builder.facing;
videoCodec = builder.videoCodec; videoCodec = builder.videoCodec;
audioCodec = builder.audioCodec;
audio = builder.audio; audio = builder.audio;
maxSize = builder.maxSize; maxSize = builder.maxSize;
maxDuration = builder.maxDuration; maxDuration = builder.maxDuration;
@ -130,9 +138,25 @@ public class VideoResult {
*/ */
@NonNull @NonNull
public File getFile() { public File getFile() {
if (file == null) {
throw new RuntimeException("File is only available when takeVideo(File) is used.");
}
return file; return file;
} }
/**
* Returns the file descriptor where the video was saved.
*
* @return the File Descriptor of this video
*/
@NonNull
public FileDescriptor getFileDescriptor() {
if (fileDescriptor == null) {
throw new RuntimeException("FileDescriptor is only available when takeVideo(FileDescriptor) is used.");
}
return fileDescriptor;
}
/** /**
* Returns the facing value with which this video was recorded. * Returns the facing value with which this video was recorded.
* *
@ -153,6 +177,16 @@ public class VideoResult {
return videoCodec; return videoCodec;
} }
/**
* Returns the codec that was used to encode the audio frames.
*
* @return the audio codec
*/
@NonNull
public AudioCodec getAudioCodec() {
return audioCodec;
}
/** /**
* Returns the max file size in bytes that was set before recording, * Returns the max file size in bytes that was set before recording,
* or 0 if no constraint was set. * or 0 if no constraint was set.

@ -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;
@ -52,6 +52,7 @@ import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
@ -99,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);
@ -158,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);
} }
@ -193,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);
} }
@ -208,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);
@ -227,11 +256,16 @@ 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
getFrameManager().setUp(PREVIEW_FORMAT, mPreviewStreamSize); getFrameManager().setUp(PREVIEW_FORMAT, mPreviewStreamSize, getAngles());
LOG.i("onStartPreview", "Starting preview with startPreview()."); LOG.i("onStartPreview", "Starting preview with startPreview().");
try { try {
@ -356,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();
@ -396,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.");
@ -424,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);
} }
@ -590,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
@ -622,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
@ -715,6 +753,7 @@ public class Camera1Engine extends CameraBaseEngine implements
private boolean applyPreviewFrameRate(@NonNull Camera.Parameters params, private boolean applyPreviewFrameRate(@NonNull Camera.Parameters params,
float oldPreviewFrameRate) { float oldPreviewFrameRate) {
List<int[]> fpsRanges = params.getSupportedPreviewFpsRange(); List<int[]> fpsRanges = params.getSupportedPreviewFpsRange();
sortRanges(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 (int[] fpsRange : fpsRanges) { for (int[] fpsRange : fpsRanges) {
@ -745,6 +784,24 @@ public class Camera1Engine extends CameraBaseEngine implements
return false; return false;
} }
private void sortRanges(List<int[]> fpsRanges) {
if (getPreviewFrameRateExact() && mPreviewFrameRate != 0F) { // sort by range width in ascending order
Collections.sort(fpsRanges, new Comparator<int[]>() {
@Override
public int compare(int[] range1, int[] range2) {
return (range1[1] - range1[0]) - (range2[1] - range2[0]);
}
});
} else { // sort by range width in descending order
Collections.sort(fpsRanges, new Comparator<int[]>() {
@Override
public int compare(int[] range1, int[] range2) {
return (range2[1] - range2[0]) - (range1[1] - range1[0]);
}
});
}
}
@Override @Override
public void setPictureFormat(@NonNull PictureFormat pictureFormat) { public void setPictureFormat(@NonNull PictureFormat pictureFormat) {
if (pictureFormat != PictureFormat.JPEG) { if (pictureFormat != PictureFormat.JPEG) {
@ -795,9 +852,7 @@ public class Camera1Engine extends CameraBaseEngine implements
// Seen this happen in logs. // Seen this happen in logs.
return; return;
} }
int rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Frame frame = getFrameManager().getFrame(data, System.currentTimeMillis());
Axis.RELATIVE_TO_SENSOR);
Frame frame = getFrameManager().getFrame(data, System.currentTimeMillis(), rotation);
if (frame != null) { if (frame != null) {
getCallback().dispatchFrame(frame); getCallback().dispatchFrame(frame);
} }
@ -826,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,17 +60,20 @@ 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;
import com.otaliastudios.cameraview.video.SnapshotVideoRecorder; import com.otaliastudios.cameraview.video.SnapshotVideoRecorder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@ -82,7 +85,8 @@ public class Camera2Engine extends CameraBaseEngine implements
ActionHolder { ActionHolder {
private static final int FRAME_PROCESSING_FORMAT = ImageFormat.YUV_420_888; private static final int FRAME_PROCESSING_FORMAT = ImageFormat.YUV_420_888;
@VisibleForTesting static final long METER_TIMEOUT = 2500; @VisibleForTesting static final long METER_TIMEOUT = 5000;
private static final long METER_TIMEOUT_SHORT = 2500;
private final CameraManager mManager; private final CameraManager mManager;
private String mCameraId; private String mCameraId;
@ -225,6 +229,15 @@ public class Camera2Engine extends CameraBaseEngine implements
} }
} }
/**
* Can be changed to select something different than {@link CameraDevice#TEMPLATE_PREVIEW}
* for the default repeating request.
* @return the default template for preview
*/
protected int getRepeatingRequestDefaultTemplate() {
return CameraDevice.TEMPLATE_PREVIEW;
}
/** /**
* Applies the repeating request builder to the preview, assuming we actually have a preview * 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.
@ -416,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;
@ -466,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
@ -486,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() {
@ -576,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
@ -607,7 +627,7 @@ public class Camera2Engine extends CameraBaseEngine implements
mPreview.setStreamSize(previewSizeForView.getWidth(), previewSizeForView.getHeight()); mPreview.setStreamSize(previewSizeForView.getWidth(), previewSizeForView.getHeight());
mPreview.setDrawRotation(getAngles().offset(Reference.BASE, Reference.VIEW, Axis.ABSOLUTE)); mPreview.setDrawRotation(getAngles().offset(Reference.BASE, Reference.VIEW, Axis.ABSOLUTE));
if (hasFrameProcessors()) { if (hasFrameProcessors()) {
getFrameManager().setUp(mFrameProcessingFormat, mFrameProcessingSize); getFrameManager().setUp(mFrameProcessingFormat, mFrameProcessingSize, getAngles());
} }
LOG.i("onStartPreview:", "Starting preview."); LOG.i("onStartPreview:", "Starting preview.");
@ -769,7 +789,7 @@ public class Camera2Engine extends CameraBaseEngine implements
boolean doMetering) { boolean doMetering) {
if (doMetering) { if (doMetering) {
LOG.i("onTakePictureSnapshot:", "doMetering is true. Delaying."); LOG.i("onTakePictureSnapshot:", "doMetering is true. Delaying.");
Action action = Actions.timeout(METER_TIMEOUT, createMeterAction(null)); Action action = Actions.timeout(METER_TIMEOUT_SHORT, createMeterAction(null));
action.addCallback(new CompletionCallback() { action.addCallback(new CompletionCallback() {
@Override @Override
protected void onActionCompleted(@NonNull Action action) { protected void onActionCompleted(@NonNull Action action) {
@ -782,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();
} }
} }
@ -802,7 +821,7 @@ public class Camera2Engine extends CameraBaseEngine implements
protected void onTakePicture(@NonNull final PictureResult.Stub stub, boolean doMetering) { protected void onTakePicture(@NonNull final PictureResult.Stub stub, boolean doMetering) {
if (doMetering) { if (doMetering) {
LOG.i("onTakePicture:", "doMetering is true. Delaying."); LOG.i("onTakePicture:", "doMetering is true. Delaying.");
Action action = Actions.timeout(METER_TIMEOUT, createMeterAction(null)); Action action = Actions.timeout(METER_TIMEOUT_SHORT, createMeterAction(null));
action.addCallback(new CompletionCallback() { action.addCallback(new CompletionCallback() {
@Override @Override
protected void onActionCompleted(@NonNull Action action) { protected void onActionCompleted(@NonNull Action action) {
@ -907,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.");
@ -918,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);
} }
@ -989,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) {
@ -1258,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
@ -1314,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
@ -1373,13 +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[]{});
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;
@ -1391,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;
@ -1402,6 +1411,35 @@ public class Camera2Engine extends CameraBaseEngine implements
return false; return false;
} }
private void sortFrameRateRanges(@NonNull Range<Integer>[] fpsRanges) {
final boolean ascending = getPreviewFrameRateExact() && mPreviewFrameRate != 0;
Arrays.sort(fpsRanges, new Comparator<Range<Integer>>() {
@Override
public int compare(Range<Integer> range1, Range<Integer> range2) {
if (ascending) {
return (range1.getUpper() - range1.getLower())
- (range2.getUpper() - range2.getLower());
} else {
return (range2.getUpper() - range2.getLower())
- (range1.getUpper() - range1.getLower());
}
}
});
}
@NonNull
protected List<Range<Integer>> filterFrameRateRanges(@NonNull Range<Integer>[] fpsRanges) {
List<Range<Integer>> results = new ArrayList<>();
int min = Math.round(mCameraOptions.getPreviewFrameRateMinValue());
int max = Math.round(mCameraOptions.getPreviewFrameRateMaxValue());
for (Range<Integer> fpsRange : fpsRanges) {
if (!fpsRange.contains(min) && !fpsRange.contains(max)) continue;
if (!FpsRangeValidator.validate(fpsRange)) continue;
results.add(fpsRange);
}
return results;
}
@Override @Override
public void setPictureFormat(final @NonNull PictureFormat pictureFormat) { public void setPictureFormat(final @NonNull PictureFormat pictureFormat) {
if (pictureFormat != mPictureFormat) { if (pictureFormat != mPictureFormat) {
@ -1441,10 +1479,7 @@ public class Camera2Engine extends CameraBaseEngine implements
// After preview, the frame manager is correctly set up // After preview, the frame manager is correctly set up
//noinspection unchecked //noinspection unchecked
Frame frame = getFrameManager().getFrame(image, Frame frame = getFrameManager().getFrame(image,
System.currentTimeMillis(), System.currentTimeMillis());
getAngles().offset(Reference.SENSOR,
Reference.OUTPUT,
Axis.RELATIVE_TO_SENSOR));
if (frame != null) { if (frame != null) {
LOG.v("onImageAvailable:", "Image acquired, dispatching."); LOG.v("onImageAvailable:", "Image acquired, dispatching.");
getCallback().dispatchFrame(frame); getCallback().dispatchFrame(frame);

@ -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;
@ -38,6 +36,7 @@ import com.otaliastudios.cameraview.size.SizeSelectors;
import com.otaliastudios.cameraview.video.VideoRecorder; import com.otaliastudios.cameraview.video.VideoRecorder;
import java.io.File; import java.io.File;
import java.io.FileDescriptor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -48,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;
@ -60,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;
@ -69,6 +72,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
@SuppressWarnings("WeakerAccess") protected boolean mPictureMetering; @SuppressWarnings("WeakerAccess") protected boolean mPictureMetering;
@SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering; @SuppressWarnings("WeakerAccess") protected boolean mPictureSnapshotMetering;
@SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate; @SuppressWarnings("WeakerAccess") protected float mPreviewFrameRate;
@SuppressWarnings("WeakerAccess") private boolean mPreviewFrameRateExact;
private FrameManager mFrameManager; private FrameManager mFrameManager;
private final Angles mAngles = new Angles(); private final Angles mAngles = new Angles();
@ -241,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;
@ -440,6 +455,16 @@ public abstract class CameraBaseEngine extends CameraEngine {
return mPictureFormat; return mPictureFormat;
} }
@Override
public final void setPreviewFrameRateExact(boolean previewFrameRateExact) {
mPreviewFrameRateExact = previewFrameRateExact;
}
@Override
public final boolean getPreviewFrameRateExact() {
return mPreviewFrameRateExact;
}
@Override @Override
public final float getPreviewFrameRate() { public final float getPreviewFrameRate() {
return mPreviewFrameRate; return mPreviewFrameRate;
@ -528,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));
} }
@ -549,7 +574,9 @@ public abstract class CameraBaseEngine extends CameraEngine {
} }
@Override @Override
public final void takeVideo(final @NonNull VideoResult.Stub stub, final @NonNull File file) { public final void takeVideo(final @NonNull VideoResult.Stub stub,
final @Nullable File file,
final @Nullable FileDescriptor fileDescriptor) {
getOrchestrator().scheduleStateful("take video", CameraState.BIND, new Runnable() { getOrchestrator().scheduleStateful("take video", CameraState.BIND, new Runnable() {
@Override @Override
public void run() { public void run() {
@ -558,9 +585,16 @@ public abstract class CameraBaseEngine extends CameraEngine {
if (mMode == Mode.PICTURE) { if (mMode == Mode.PICTURE) {
throw new IllegalStateException("Can't record video while in PICTURE mode"); throw new IllegalStateException("Can't record video while in PICTURE mode");
} }
stub.file = file; if (file != null) {
stub.file = file;
} else if (fileDescriptor != null) {
stub.fileDescriptor = fileDescriptor;
} else {
throw new IllegalStateException("file and fileDescriptor are both null.");
}
stub.isSnapshot = false; stub.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;
@ -588,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;
@ -49,6 +49,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import java.io.File; import java.io.File;
import java.io.FileDescriptor;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -115,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);
@ -637,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();
@ -689,6 +693,8 @@ public abstract class CameraEngine implements
public abstract void setPictureFormat(@NonNull PictureFormat pictureFormat); public abstract void setPictureFormat(@NonNull PictureFormat pictureFormat);
@NonNull public abstract PictureFormat getPictureFormat(); @NonNull public abstract PictureFormat getPictureFormat();
public abstract void setPreviewFrameRateExact(boolean previewFrameRateExact);
public abstract boolean getPreviewFrameRateExact();
public abstract void setPreviewFrameRate(float previewFrameRate); public abstract void setPreviewFrameRate(float previewFrameRate);
public abstract float getPreviewFrameRate(); public abstract float getPreviewFrameRate();
@ -712,7 +718,9 @@ public abstract class CameraEngine implements
public abstract void takePictureSnapshot(final @NonNull PictureResult.Stub stub); public abstract void takePictureSnapshot(final @NonNull PictureResult.Stub stub);
public abstract boolean isTakingVideo(); public abstract boolean isTakingVideo();
public abstract void takeVideo(@NonNull VideoResult.Stub stub, @NonNull File file); public abstract void takeVideo(@NonNull VideoResult.Stub stub,
@Nullable File file,
@Nullable FileDescriptor fileDescriptor);
public abstract void takeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull File file); public abstract void takeVideoSnapshot(@NonNull VideoResult.Stub stub, @NonNull File file);
public abstract void stopVideo(); public abstract void stopVideo();

@ -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);
} }
} }

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

@ -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
@ -177,8 +152,8 @@ public abstract class BaseFilter implements Filter {
} }
@Override @Override
public void draw(long timestampUs, 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 {
@ -188,44 +163,19 @@ public abstract class BaseFilter implements Filter {
} }
} }
protected void onPreDraw(long timestampUs, 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();

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

@ -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);
} }
} }
} }
@ -237,16 +220,17 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
} }
@Override @Override
public void draw(long timestampUs, float[] transformMatrix) { public void draw(long timestampUs, @NonNull float[] transformMatrix) {
synchronized (lock) { synchronized (lock) {
for (int i = 0; i < filters.size(); i++) { for (int i = 0; i < filters.size(); i++) {
boolean isFirst = i == 0; boolean isFirst = i == 0;
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
@ -117,9 +117,9 @@ public class AutoFixFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, 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
@ -86,9 +86,9 @@ public class BrightnessFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, 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
@ -88,9 +88,9 @@ public class ContrastFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, 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
@ -93,7 +93,7 @@ public class DocumentaryFilter extends BaseFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
float[] scale = new float[2]; float[] scale = new float[2];
if (mWidth > mHeight) { if (mWidth > mHeight) {
@ -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,13 +131,13 @@ 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
protected void onPreDraw(long timestampUs, float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
float[] first = new float[]{ float[] first = new float[]{
Color.red(mFirstColor) / 255f, Color.red(mFirstColor) / 255f,
@ -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
@ -95,17 +95,17 @@ public class FillLightFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
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
@ -83,9 +83,9 @@ public class GammaFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, 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
@ -138,13 +138,13 @@ public class GrainFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, 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
@ -96,11 +96,11 @@ public class HueFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
// 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
@ -141,7 +141,7 @@ public class LomoishFilter extends BaseFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
float[] scale = new float[2]; float[] scale = new float[2];
if (width > height) { if (width > height) {
@ -153,12 +153,12 @@ public class LomoishFilter extends BaseFilter {
} }
float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f; 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
@ -106,22 +106,22 @@ public class SaturationFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
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
@ -116,13 +116,13 @@ public class SharpnessFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, 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
@ -94,9 +94,9 @@ public class TemperatureFilter extends BaseFilter implements OneParameterFilter
} }
@Override @Override
protected void onPreDraw(long timestampUs, 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
@ -91,7 +91,7 @@ public class TintFilter extends BaseFilter implements OneParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
float[] channels = new float[]{ float[] channels = new float[]{
Color.red(tint) / 255f, Color.red(tint) / 255f,
@ -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
@ -145,7 +145,7 @@ public class VignetteFilter extends BaseFilter implements TwoParameterFilter {
} }
@Override @Override
protected void onPreDraw(long timestampUs, float[] transformMatrix) { protected void onPreDraw(long timestampUs, @NonNull float[] transformMatrix) {
super.onPreDraw(timestampUs, transformMatrix); super.onPreDraw(timestampUs, transformMatrix);
float[] scale = new float[2]; float[] scale = new float[2];
if (mWidth > mHeight) { if (mWidth > mHeight) {
@ -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");
} }
} }

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

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

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

@ -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;

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

@ -1,4 +1,4 @@
package com.otaliastudios.cameraview.internal.utils; package com.otaliastudios.cameraview.internal;
import androidx.exifinterface.media.ExifInterface; 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;
@ -37,6 +73,8 @@ public class OrientationHelper {
@VisibleForTesting @VisibleForTesting
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.
@ -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;
@ -81,7 +81,7 @@ public class Snapshot1PictureRecorder extends SnapshotPictureRecorder {
// It seems that the buffers are already cleared here, so we need to allocate again. // It seems that the buffers are already cleared here, so we need to allocate again.
camera.setPreviewCallbackWithBuffer(null); // Release anything left camera.setPreviewCallbackWithBuffer(null); // Release anything left
camera.setPreviewCallbackWithBuffer(mEngine1); // Add ourselves camera.setPreviewCallbackWithBuffer(mEngine1); // Add ourselves
mEngine1.getFrameManager().setUp(mFormat, previewStreamSize); mEngine1.getFrameManager().setUp(mFormat, previewStreamSize, mEngine1.getAngles());
} }
}); });
} }

@ -18,7 +18,7 @@ import com.otaliastudios.cameraview.engine.action.Actions;
import com.otaliastudios.cameraview.engine.action.BaseAction; import com.otaliastudios.cameraview.engine.action.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(

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

Loading…
Cancel
Save