Compare commits

..

40 Commits
egloo2 ... main

Author SHA1 Message Date
DeweyReed e26ac950be
Fix the setSize typo (#1198) 2 years ago
Dmitry Naymushin ae3f3da0e1
A couple of fixes and docs improvements (#1184) 2 years ago
Steve Xu 82ea49f6e2
#1088: add Pixel 4a into FpsRangeValidator (#1164) 3 years ago
Mattia Iavarone 0057981408
Release v2.7.2 (#1140) 3 years ago
Mattia Iavarone 399844321b
Fix orientation when activity handles rotations (#1117) 3 years ago
Mattia Iavarone c2e02923de Fix sonatype build 3 years ago
Mattia Iavarone 994b1dd434
Release 2.7.1 (#1111) 3 years ago
Jan Šmuk ceb0eca394
Add Pixel 4a into FpsRangeValidator (#1089) 4 years ago
Ryan Loebs bde88a946e
Add option to capture hardware accelerated overlays (#1066) 4 years ago
Ryan Loebs f608ec3918
Add flag to disable automatic gesture handling (#1068) 4 years ago
Mattia Iavarone a219073468
Release v2.7.0 (#1040) 4 years ago
Ezequiel Adrián d89491195c
Implement Shutter callback. Add GestureAction.TAKE_PICTURE_SNAPSHOT. (#1030) 4 years ago
Michael 5d10277d7b
add try catch to catch exceptions when takePicture fails (#1024) 4 years ago
Mattia Iavarone 0f6ab20c3e
Make filterFrameRateRanges protected (#1026) 4 years ago
Billy Sutomo a9fcd6506e
Update frame-processing.md (#941) 4 years ago
Philippe Banwarth 6aa2d3a489
Log errors when writing the image to a file (#960) 4 years ago
Mattia Iavarone 40ace54c19
Fix preview on Pixel devices, fix deadlock with surface preview (#1020) 4 years ago
Mattia Iavarone 2429cc114b
Add CameraEngine.getRepeatingRequestDefaultTemplate (#1004) 4 years ago
Mattia Iavarone 0001ab7a54
Rewrite orchestrator (#992) 4 years ago
Mattia Iavarone e3fcef286f
Release v2.6.4 (#954) 4 years ago
Mattia Iavarone 8207e67679
Some bug fixes (#953) 4 years ago
thijsonline daf7a0bf44
Fix for #927 OrientationHelper.enable() called twice (#928) 4 years ago
Steve 170d87ed39
Fix references on overlay documentation (#931) 4 years ago
Mattia Iavarone a7cf726636 Fix isMinifyEnabled 4 years ago
Mattia Iavarone 610f511a76
Release v2.6.3 (#912) 4 years ago
Mattia Iavarone a7c18324a5
Build tools update (#911) 4 years ago
Alexander 66c37373bd
Wrapped some crashes which happened on start camera and which connect with params. (#897) 4 years ago
Alexander 8455c23fb1
Wrapped some crashes which connect with empty camera params (#851) 4 years ago
jeffreyfjohnson b5921ec6bf
Restart Camera1 Frame Processing After Taking a Picture - Fixes Issue 824 (#877) 4 years ago
Ryan McClarnon 4364ec7c3a
Add capability to set audio codec to encode video files with. (#861) 5 years ago
Mattia Iavarone edbade24a9
Preview interface changes (#816) 5 years ago
Alexander 5530c011fa
Add ability to clear lifecycleObserver (#798) 5 years ago
Mattia Iavarone 778cdedc00
Release v2.6.2 (#794) 5 years ago
dependabot[bot] fa26b2a654
Bump nokogiri from 1.10.4 to 1.10.8 in /docs (#786) 5 years ago
Mattia Iavarone 124aee6908 Make LogAction log to info 5 years ago
Alexander 218b27319d
Add setter for flag enabled/disabled auto check permissions (#775) 5 years ago
Alexander 858495afde
Make CameraLogger ThreadSafe (#779) 5 years ago
Mattia Iavarone aa95d19902 Ads 5 years ago
Mattia Iavarone 968d181af4
GL cleanup (#766) 5 years ago
dependabot[bot] 7f23c309ab Bump rubyzip from 1.2.2 to 2.0.0 in /docs (#759) 5 years ago
  1. 3
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 2
      .github/ISSUE_TEMPLATE/question.md
  3. 12
      .github/pull_request_template.md
  4. 52
      .github/workflows/build.yml
  5. 21
      .github/workflows/deploy.yml
  6. 2
      .github/workflows/emulator_script.sh
  7. 25
      .github/workflows/snapshot.yml
  8. 23
      .run/runAndroidTests.run.xml
  9. 23
      .run/runUnitTests.run.xml
  10. 2
      LICENSE
  11. 4
      README.md
  12. 30
      build.gradle
  13. 30
      build.gradle.kts
  14. 276
      cameraview/build.gradle
  15. 137
      cameraview/build.gradle.kts
  16. 8
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewCallbacksTest.java
  17. 39
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  18. 19
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java
  19. 14
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  20. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java
  21. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/MultiFilterTest.java
  22. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/markers/MarkerLayoutTest.java
  23. 7
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java
  24. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayLayoutTest.java
  25. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
  26. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/BitmapCallback.java
  27. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraListener.java
  28. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraLogger.java
  29. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  30. 1
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraUtils.java
  31. 118
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  32. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java
  33. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/AudioCodec.java
  34. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java
  35. 70
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  36. 102
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  37. 27
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java
  38. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  39. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/LogAction.java
  40. 222
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraOrchestrator.java
  41. 10
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/orchestrator/CameraStateOrchestrator.java
  42. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/MultiFilter.java
  43. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java
  44. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/FpsRangeValidator.java
  45. 51
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/OrientationHelper.java
  46. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/Overlay.java
  47. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayDrawer.java
  48. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/overlay/OverlayLayout.java
  49. 36
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full1PictureRecorder.java
  50. 7
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Full2PictureRecorder.java
  51. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java
  52. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
  53. 32
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java
  54. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/FilterCameraPreview.java
  55. 19
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
  56. 23
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererCameraPreview.java
  57. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererFrameCallback.java
  58. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreview.java
  59. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.java
  60. 28
      cameraview/src/main/java/com/otaliastudios/cameraview/size/AspectRatio.java
  61. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java
  62. 35
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  63. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/MediaEncoder.java
  64. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java
  65. 35
      cameraview/src/main/res/values/attrs.xml
  66. 27
      demo/build.gradle
  67. 23
      demo/build.gradle.kts
  68. 3
      demo/src/main/AndroidManifest.xml
  69. 417
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  70. 46
      demo/src/main/java/com/otaliastudios/cameraview/demo/MessageView.java
  71. 599
      demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java
  72. 97
      demo/src/main/java/com/otaliastudios/cameraview/demo/OptionView.java
  73. 136
      demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java
  74. 134
      demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java
  75. 344
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/CameraActivity.kt
  76. 34
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/MessageView.kt
  77. 296
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/Option.kt
  78. 76
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/OptionView.kt
  79. 105
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/PicturePreviewActivity.kt
  80. 107
      demo/src/main/kotlin/com/otaliastudios/cameraview/demo/VideoPreviewActivity.kt
  81. 5
      demo/src/main/res/layout/activity_video_preview.xml
  82. 1
      docs/.gitignore
  83. 248
      docs/Gemfile.lock
  84. 80
      docs/_about/changelog.md
  85. 95
      docs/_about/faq.md
  86. 33
      docs/_about/install.md
  87. 4
      docs/_config.yml
  88. 11
      docs/_docs/capturing-media.md
  89. 18
      docs/_docs/controls.md
  90. 2
      docs/_docs/filters.md
  91. 4
      docs/_docs/frame-processing.md
  92. 9
      docs/_docs/gestures.md
  93. 4
      docs/_docs/metering.md
  94. 4
      docs/_docs/preview-size.md
  95. 2
      docs/_docs/runtime-permissions.md
  96. 20
      docs/_docs/watermarks-and-overlays.md
  97. 2
      docs/_extra/contributing.md
  98. 6
      docs/_extra/v1-migration-guide.md
  99. 1
      docs/_layouts/main.html
  100. 11
      docs/_layouts/page.html
  101. Some files were not shown because too many files have changed in this diff Show More

@ -13,11 +13,12 @@ Please add a clear description of what the bug is, **and** fill the list below.
- Camera engine used: *camera1/camera2/both*
- Reproducible in official demo app: *yes/no*
- Device / Android version: *Pixel, API 28*
- I have read the [FAQ page](https://natario1.github.io/CameraView/about/faq): *yes/no*
### To Reproduce
Steps to reproduce the behavior, possibly in the demo app:
1. Go to '...'
2. Click on '....'
2. Click on '...'
3. See error
### Expected behavior

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

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

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

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

@ -11,4 +11,4 @@ ADB_TAGS="$ADB_TAGS MediaEncoderEngine:I MediaEncoder:I AudioMediaEncoder:I Vide
ADB_TAGS="$ADB_TAGS CameraIntegrationTest:I MessageQueue:W MPEG4Writer:I"
adb logcat -c
adb logcat $ADB_TAGS *:E -v color &
./gradlew cameraview:connectedCheck
./gradlew cameraview:runAndroidTests

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

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

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

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

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

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

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

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

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

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

@ -1080,6 +1080,20 @@ public abstract class CameraIntegrationTest<E extends CameraBaseEngine> extends
assert15Frames(processor);
}
@Test
@Retry(emulatorOnly = true)
@SdkExclude(maxSdkVersion = 22, emulatorOnly = true)
public void testFrameProcessing_afterPicture() throws Exception {
FrameProcessor processor = mock(FrameProcessor.class);
camera.addFrameProcessor(processor);
openSync(true);
camera.takePicture();
waitForPictureResult(true);
assert15Frames(processor);
}
@Test
@Retry(emulatorOnly = true)
@SdkExclude(maxSdkVersion = 22, emulatorOnly = true)

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

@ -179,7 +179,7 @@ public class MultiFilterTest extends BaseEglTest {
assertTrue(state.isFramebufferCreated);
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0);
assertTrue(result[0] != 0);
// assertTrue(result[0] != 0);
return null;
}
}).when(filter1).draw(0L, matrix);

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

@ -7,6 +7,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.size.Size;
import org.hamcrest.BaseMatcher;
@ -69,9 +70,9 @@ public class OverlayDrawerTest extends BaseEglTest {
@Test
public void testRelease() {
OverlayDrawer drawer = new OverlayDrawer(mock(Overlay.class), new Size(WIDTH, HEIGHT));
drawer.mTextureDrawer = spy(drawer.mTextureDrawer);
GlTextureDrawer textureDrawer = spy(drawer.mTextureDrawer);
drawer.mTextureDrawer = textureDrawer;
drawer.release();
verify(drawer.mTextureDrawer, times(1)).release();
verify(textureDrawer, times(1)).release();
}
}

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

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

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

@ -161,4 +161,13 @@ public abstract class CameraListener {
}
/**
* Notifies that the picture capture has started. Can be used to update the UI for visual
* confirmation or sound effects.
*/
@UiThread
public void onPictureShutter() {
}
}

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

@ -4,6 +4,7 @@ package com.otaliastudios.cameraview;
import android.graphics.ImageFormat;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing;
@ -105,6 +106,8 @@ public abstract class CameraOptions {
return (Collection<T>) Arrays.asList(Mode.values());
} else if (controlClass.equals(VideoCodec.class)) {
return (Collection<T>) Arrays.asList(VideoCodec.values());
} else if (controlClass.equals(AudioCodec.class)) {
return (Collection<T>) Arrays.asList(AudioCodec.values());
} else if (controlClass.equals(WhiteBalance.class)) {
return (Collection<T>) getSupportedWhiteBalance();
} else if (controlClass.equals(Engine.class)) {

@ -94,6 +94,7 @@ public class CameraUtils {
stream.flush();
return file;
} catch (IOException e) {
LOG.e("writeToFile:", "could not write file.", e);
return null;
}
}

@ -33,6 +33,7 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.controls.ControlParser;
import com.otaliastudios.cameraview.controls.Engine;
@ -132,6 +133,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
private Engine mEngine;
private Filter mPendingFilter;
private int mFrameProcessingExecutors;
private int mActiveGestures;
// Components
private Handler mUiHandler;
@ -225,6 +227,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
int frameExecutors = a.getInteger(R.styleable.CameraView_cameraFrameProcessingExecutors,
DEFAULT_FRAME_PROCESSING_EXECUTORS);
boolean drawHardwareOverlays = a.getBoolean(R.styleable.CameraView_cameraDrawHardwareOverlays, false);
// Size selectors and gestures
SizeSelectorParser sizeSelectors = new SizeSelectorParser(a);
GestureParser gestures = new GestureParser(a);
@ -258,6 +262,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setUseDeviceOrientation(useDeviceOrientation);
setGrid(controls.getGrid());
setGridColor(gridColor);
setDrawHardwareOverlays(drawHardwareOverlays);
// Apply camera engine params
// Adding new ones? See setEngine().
@ -268,6 +273,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setHdr(controls.getHdr());
setAudio(controls.getAudio());
setAudioBitRate(audioBitRate);
setAudioCodec(controls.getAudioCodec());
setPictureSize(sizeSelectors.getPictureSizeSelector());
setPictureMetering(pictureMetering);
setPictureSnapshotMetering(pictureSnapshotMetering);
@ -392,12 +398,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// attached. That's why we instantiate the preview here.
doInstantiatePreview();
}
mOrientationHelper.enable();
}
@Override
protected void onDetachedFromWindow() {
if (!mInEditor) mOrientationHelper.disable();
mLastPreviewStreamSize = null;
super.onDetachedFromWindow();
}
@ -605,6 +609,12 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
mGestureMap.get(Gesture.SCROLL_VERTICAL) != none);
break;
}
mActiveGestures = 0;
for(GestureAction act : mGestureMap.values()) {
mActiveGestures += act == GestureAction.NONE ? 0 : 1;
}
return true;
}
mapGesture(gesture, none);
@ -633,7 +643,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true; // Steal our own events.
// Steal our own events if gestures are enabled
return mActiveGestures > 0;
}
@SuppressLint("ClickableViewAccessibility")
@ -669,6 +680,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//noinspection ConstantConditions
switch (action) {
case TAKE_PICTURE_SNAPSHOT:
takePictureSnapshot();
break;
case TAKE_PICTURE:
takePicture();
break;
@ -726,6 +741,15 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//region Lifecycle APIs
/**
* Sets permissions flag if you want enable auto check permissions or disable it.
* @param requestPermissions - true: auto check permissions enabled, false: auto check permissions disabled.
*/
@SuppressWarnings("unused")
public void setRequestPermissions(boolean requestPermissions) {
mRequestPermissions = requestPermissions;
}
/**
* Returns whether the camera engine has started.
* @return whether the camera has started
@ -744,13 +768,27 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* Sets the lifecycle owner for this view. This means you don't need
* to call {@link #open()}, {@link #close()} or {@link #destroy()} at all.
*
* If you want that lifecycle stopped controlling the state of the camera,
* pass null in this method.
*
* @param owner the owner activity or fragment
*/
public void setLifecycleOwner(@NonNull LifecycleOwner owner) {
if (mLifecycle != null) mLifecycle.removeObserver(this);
public void setLifecycleOwner(@Nullable LifecycleOwner owner) {
if (owner == null) {
clearLifecycleObserver();
} else {
clearLifecycleObserver();
mLifecycle = owner.getLifecycle();
mLifecycle.addObserver(this);
}
}
private void clearLifecycleObserver() {
if (mLifecycle != null) {
mLifecycle.removeObserver(this);
mLifecycle = null;
}
}
/**
* Starts the camera preview, if not started already.
@ -831,6 +869,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void close() {
if (mInEditor) return;
mOrientationHelper.disable();
mCameraEngine.stop(false);
if (mCameraPreview != null) mCameraPreview.onPause();
}
@ -884,6 +923,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setWhiteBalance((WhiteBalance) control);
} else if (control instanceof VideoCodec) {
setVideoCodec((VideoCodec) control);
} else if (control instanceof AudioCodec) {
setAudioCodec((AudioCodec) control);
} else if (control instanceof Preview) {
setPreview((Preview) control);
} else if (control instanceof Engine) {
@ -920,6 +961,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return (T) getWhiteBalance();
} else if (controlClass == VideoCodec.class) {
return (T) getVideoCodec();
} else if (controlClass == AudioCodec.class) {
return (T) getAudioCodec();
} else if (controlClass == Preview.class) {
return (T) getPreview();
} else if (controlClass == Engine.class) {
@ -992,6 +1035,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setHdr(oldEngine.getHdr());
setAudio(oldEngine.getAudio());
setAudioBitRate(oldEngine.getAudioBitRate());
setAudioCodec(oldEngine.getAudioCodec());
setPictureSize(oldEngine.getPictureSizeSelector());
setPictureFormat(oldEngine.getPictureFormat());
setVideoSize(oldEngine.getVideoSizeSelector());
@ -1621,6 +1665,30 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.getAudioBitRate();
}
/**
* Sets the encoder for audio recordings.
* Defaults to {@link AudioCodec#DEVICE_DEFAULT}.
*
* @see AudioCodec#DEVICE_DEFAULT
* @see AudioCodec#AAC
* @see AudioCodec#HE_AAC
* @see AudioCodec#AAC_ELD
*
* @param codec requested audio codec
*/
public void setAudioCodec(@NonNull AudioCodec codec) {
mCameraEngine.setAudioCodec(codec);
}
/**
* Gets the current encoder for audio recordings.
* @return the current audio codec
*/
@NonNull
public AudioCodec getAudioCodec() {
return mCameraEngine.getAudioCodec();
}
/**
* Adds a {@link CameraListener} instance to be notified of all
* interesting events that happen during the camera lifecycle.
@ -1756,6 +1824,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* @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);
}
@ -2090,6 +2159,25 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.isTakingPicture();
}
/**
* Sets the overlay layout hardware canvas capture mode to allow hardware
* accelerated views to be captured in snapshots
*
* @param on true if enabled
*/
public void setDrawHardwareOverlays(boolean on) {
mOverlayLayout.setHardwareCanvasEnabled(on);
}
/**
* Returns true if the overlay layout is set to capture the hardware canvas
* of child views
*
* @return boolean indicating hardware canvas capture is enabled
*/
public boolean getDrawHardwareOverlays() {
return mOverlayLayout.getHardwareCanvasEnabled();
}
//endregion
//region Callbacks and dispatching
@ -2170,10 +2258,18 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
@Override
public void onShutter(boolean shouldPlaySound) {
public void dispatchOnPictureShutter(boolean shouldPlaySound) {
if (shouldPlaySound && mPlaySounds) {
playSound(MediaActionSound.SHUTTER_CLICK);
}
mUiHandler.post(new Runnable() {
@Override
public void run() {
for (CameraListener listener : mListeners) {
listener.onPictureShutter();
}
}
});
}
@Override
@ -2274,12 +2370,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
}
@Override
public void onDisplayOffsetChanged(int displayOffset, boolean willRecreate) {
LOG.i("onDisplayOffsetChanged", displayOffset, "recreate:", willRecreate);
if (isOpened() && !willRecreate) {
// Display offset changes when the device rotation lock is off and the activity
// is free to rotate. However, some changes will NOT recreate the activity, namely
// 180 degrees flips. In this case, we must restart the camera manually.
public void onDisplayOffsetChanged() {
if (isOpened()) {
// We can't handle display offset (View angle) changes without restarting.
// See comments in OrientationHelper for more information.
LOG.w("onDisplayOffsetChanged", "restarting the camera.");
close();
open();

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

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

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

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

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

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

@ -17,6 +17,7 @@ import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.orchestrator.CameraOrchestrator;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
@ -115,7 +116,7 @@ public abstract class CameraEngine implements
void dispatchOnCameraOpened(@NonNull CameraOptions options);
void dispatchOnCameraClosed();
void onCameraPreviewStreamSizeChanged();
void onShutter(boolean shouldPlaySound);
void dispatchOnPictureShutter(boolean shouldPlaySound);
void dispatchOnVideoTaken(@NonNull VideoResult.Stub stub);
void dispatchOnPictureTaken(@NonNull PictureResult.Stub stub);
void dispatchOnFocusStart(@Nullable Gesture trigger, @NonNull PointF where);
@ -637,6 +638,9 @@ public abstract class CameraEngine implements
public abstract void setAudioBitRate(int audioBitRate);
public abstract int getAudioBitRate();
public abstract void setAudioCodec(@NonNull AudioCodec codec);
@NonNull public abstract AudioCodec getAudioCodec();
public abstract void setSnapshotMaxWidth(int maxWidth);
public abstract int getSnapshotMaxWidth();

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

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

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

@ -48,6 +48,7 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
static class State {
@VisibleForTesting boolean isProgramCreated = false;
@VisibleForTesting boolean isFramebufferCreated = false;
private boolean sizeChanged = false;
@VisibleForTesting Size size = null;
private int programHandle = -1;
private GlFramebuffer outputFramebuffer = null;
@ -135,8 +136,17 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
private void maybeCreateFramebuffer(@NonNull Filter filter, boolean isFirst, boolean isLast) {
State state = states.get(filter);
if (isLast) {
//noinspection ConstantConditions
if (state.isFramebufferCreated || isLast) return;
state.sizeChanged = false;
return;
}
//noinspection ConstantConditions
if (state.sizeChanged) {
maybeDestroyFramebuffer(filter);
state.sizeChanged = false;
}
if (!state.isFramebufferCreated) {
state.isFramebufferCreated = true;
state.outputTexture = new GlTexture(GLES20.GL_TEXTURE0,
GLES20.GL_TEXTURE_2D,
@ -145,6 +155,7 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
state.outputFramebuffer = new GlFramebuffer();
state.outputFramebuffer.attach(state.outputTexture);
}
}
private void maybeDestroyFramebuffer(@NonNull Filter filter) {
State state = states.get(filter);
@ -157,11 +168,13 @@ public class MultiFilter implements Filter, OneParameterFilter, TwoParameterFilt
state.outputTexture = null;
}
// Any thread...
private void maybeSetSize(@NonNull Filter filter) {
State state = states.get(filter);
//noinspection ConstantConditions
if (size != null && !size.equals(state.size)) {
state.size = size;
state.sizeChanged = true;
filter.setSize(size.getWidth(), size.getHeight());
}
}

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

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

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

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

@ -6,6 +6,7 @@ import android.graphics.PorterDuff;
import android.graphics.SurfaceTexture;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.os.Build;
import android.view.Surface;
import androidx.annotation.NonNull;
@ -63,7 +64,12 @@ public class OverlayDrawer {
*/
public void draw(@NonNull Overlay.Target target) {
try {
final Canvas surfaceCanvas = mSurface.lockCanvas(null);
final Canvas surfaceCanvas;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mOverlay.getHardwareCanvasEnabled()) {
surfaceCanvas = mSurface.lockHardwareCanvas();
} else {
surfaceCanvas = mSurface.lockCanvas(null);
}
surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mOverlay.drawOn(target, surfaceCanvas);
mSurface.unlockCanvasAndPost(surfaceCanvas);

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

@ -1,13 +1,18 @@
package com.otaliastudios.cameraview.picture;
import android.hardware.Camera;
import android.util.Log;
import androidx.annotation.NonNull;
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 androidx.annotation.NonNull;
import androidx.exifinterface.media.ExifInterface;
import com.otaliastudios.cameraview.size.Size;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -40,6 +45,8 @@ public class Full1PictureRecorder extends FullPictureRecorder {
// Stopping the preview callback is important on older APIs / emulators,
// or takePicture can hang and leave the camera in a bad state.
mCamera.setPreviewCallbackWithBuffer(null);
mEngine.getFrameManager().release();
try {
mCamera.takePicture(
new Camera.ShutterCallback() {
@Override
@ -67,13 +74,34 @@ public class Full1PictureRecorder extends FullPictureRecorder {
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);
camera.startPreview(); // This is needed, read somewhere in the docs.
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();
}
}
);
LOG.i("take() returned.");
} catch (Exception e) {
mError = e;
dispatchResult();
}
}
@Override

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

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

@ -12,14 +12,11 @@ import android.os.Build;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.overlay.OverlayDrawer;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread;
import com.otaliastudios.cameraview.filter.Filter;
@ -30,13 +27,14 @@ import com.otaliastudios.opengl.surface.EglSurface;
import com.otaliastudios.opengl.surface.EglWindowSurface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import android.view.Surface;
/**
* API 19.
* Records a picture snapshots from the {@link GlCameraPreview}. It works as follows:
* Records a picture snapshots from the {@link RendererCameraPreview}. It works as follows:
*
* - We register a one time {@link RendererFrameCallback} on the preview
* - We get the textureId and the frame callback on the {@link RendererThread}
@ -55,8 +53,7 @@ import android.view.Surface;
*/
public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
private CameraEngine mEngine;
private GlCameraPreview mPreview;
private RendererCameraPreview mPreview;
private AspectRatio mOutputRatio;
private Overlay mOverlay;
@ -66,14 +63,14 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
public SnapshotGlPictureRecorder(
@NonNull PictureResult.Stub stub,
@NonNull CameraEngine engine,
@NonNull GlCameraPreview preview,
@NonNull AspectRatio outputRatio) {
super(stub, engine);
mEngine = engine;
@Nullable PictureResultListener listener,
@NonNull RendererCameraPreview preview,
@NonNull AspectRatio outputRatio,
@Nullable Overlay overlay) {
super(stub, listener);
mPreview = preview;
mOutputRatio = outputRatio;
mOverlay = engine.getOverlay();
mOverlay = overlay;
mHasOverlay = mOverlay != null && mOverlay.drawsOn(Overlay.Target.PICTURE_SNAPSHOT);
}
@ -96,10 +93,10 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@RendererThread
@Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture,
final float scaleX,
final float scaleY) {
int rotation, float scaleX, float scaleY) {
mPreview.removeRendererFrameCallback(this);
SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture, scaleX, scaleY);
SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture,
rotation, scaleX, scaleY);
}
});
@ -129,18 +126,17 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@RendererThread
@TargetApi(Build.VERSION_CODES.KITKAT)
protected void onRendererFrame(@SuppressWarnings("unused") @NonNull final SurfaceTexture surfaceTexture,
final int rotation,
final float scaleX,
final float scaleY) {
// Get egl context from the RendererThread, which is the one in which we have created
// the textureId and the overlayTextureId, managed by the GlSurfaceView.
// Next operations can then be performed on different threads using this handle.
final EGLContext eglContext = EGL14.eglGetCurrentContext();
// Calling this invalidates the rotation/scale logic below:
// surfaceTexture.getTransformMatrix(mTransform); // TODO activate and fix the logic.
WorkerHandler.execute(new Runnable() {
@Override
public void run() {
takeFrame(surfaceTexture, scaleX, scaleY, eglContext);
takeFrame(surfaceTexture, rotation, scaleX, scaleY, eglContext);
}
});
@ -173,6 +169,7 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@WorkerThread
@TargetApi(Build.VERSION_CODES.KITKAT)
protected void takeFrame(@NonNull SurfaceTexture surfaceTexture,
int rotation,
float scaleX,
float scaleY,
@NonNull EGLContext eglContext) {
@ -189,22 +186,18 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
eglSurface.makeCurrent();
final float[] transform = mTextureDrawer.getTextureTransform();
// 2. Apply scale and crop
boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR);
float realScaleX = flip ? scaleY : scaleX;
float realScaleY = flip ? scaleX : scaleY;
float scaleTranslX = (1F - realScaleX) / 2F;
float scaleTranslY = (1F - realScaleY) / 2F;
// 2. Apply preview transformations
surfaceTexture.getTransformMatrix(transform);
float scaleTranslX = (1F - scaleX) / 2F;
float scaleTranslY = (1F - scaleY) / 2F;
Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0);
Matrix.scaleM(transform, 0, realScaleX, realScaleY, 1);
Matrix.scaleM(transform, 0, scaleX, scaleY, 1);
// 3. Apply rotation and flip
// If this doesn't work, rotate "rotation" before scaling, like GlCameraPreview does.
Matrix.translateM(transform, 0, 0.5F, 0.5F, 0); // Go back to 0,0
Matrix.rotateM(transform, 0, -mResult.rotation, 0, 0, 1); // Rotate (not sure why we need the minus)
mResult.rotation = 0;
if (mResult.facing == Facing.FRONT) { // 5. Flip horizontally for front camera
Matrix.scaleM(transform, 0, -1, 1, 1);
}
Matrix.rotateM(transform, 0, rotation + mResult.rotation, 0, 0, 1); // Rotate to OUTPUT
Matrix.scaleM(transform, 0, 1, -1, 1); // Vertical flip because we'll use glReadPixels
Matrix.translateM(transform, 0, -0.5F, -0.5F, 0); // Go back to old position
// 4. Do pretty much the same for overlays
@ -213,13 +206,12 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
mOverlayDrawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
// 2. Then we can apply the transformations
int rotation = mEngine.getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
Matrix.translateM(mOverlayDrawer.getTransform(), 0, 0.5F, 0.5F, 0);
Matrix.rotateM(mOverlayDrawer.getTransform(), 0, rotation, 0, 0, 1);
// No need to flip the x axis for front camera, but need to flip the y axis always.
Matrix.scaleM(mOverlayDrawer.getTransform(), 0, 1, -1, 1);
Matrix.rotateM(mOverlayDrawer.getTransform(), 0, mResult.rotation, 0, 0, 1);
Matrix.scaleM(mOverlayDrawer.getTransform(), 0, 1, -1, 1); // Vertical flip because we'll use glReadPixels
Matrix.translateM(mOverlayDrawer.getTransform(), 0, -0.5F, -0.5F, 0);
}
mResult.rotation = 0;
// 5. Draw and save
long timestampUs = surfaceTexture.getTimestamp() / 1000L;
@ -239,7 +231,6 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@Override
protected void dispatchResult() {
mEngine = null;
mOutputRatio = null;
super.dispatchResult();
}

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

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

@ -60,7 +60,8 @@ import javax.microedition.khronos.opengles.GL10;
* which means that we can fetch the GL context that was created and is managed
* by the {@link GLSurfaceView}.
*/
public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceTexture> {
public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture>
implements FilterCameraPreview, RendererCameraPreview {
private boolean mDispatched;
private SurfaceTexture mInputSurfaceTexture;
@ -110,7 +111,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
@NonNull
@Override
View getRootView() {
public View getRootView() {
return mRootView;
}
@ -232,7 +233,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
mOutputTextureDrawer.draw(mInputSurfaceTexture.getTimestamp() / 1000L);
for (RendererFrameCallback callback : mRendererFrameCallbacks) {
callback.onRendererFrame(mInputSurfaceTexture, mCropScaleX, mCropScaleY);
callback.onRendererFrame(mInputSurfaceTexture, mDrawRotation, mCropScaleX, mCropScaleY);
}
}
}
@ -289,11 +290,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
if (callback != null) callback.onCrop();
}
/**
* Method specific to the GL preview. Adds a {@link RendererFrameCallback}
* to receive renderer frame events.
* @param callback a callback
*/
@Override
public void addRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
getView().queueEvent(new Runnable() {
@Override
@ -308,11 +305,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
});
}
/**
* Method specific to the GL preview. Removes a {@link RendererFrameCallback}
* that was previously added to receive renderer frame events.
* @param callback a callback
*/
@Override
public void removeRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
mRendererFrameCallbacks.remove(callback);
}

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

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

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

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

@ -33,8 +33,8 @@ public class AspectRatio implements Comparable<AspectRatio> {
@NonNull
public static AspectRatio of(int x, int y) {
int gcd = gcd(x, y);
x /= gcd;
y /= gcd;
if (gcd > 0) x /= gcd;
if (gcd > 0) y /= gcd;
String key = x + ":" + y;
AspectRatio cached = sCache.get(key);
if (cached == null) {
@ -58,8 +58,8 @@ public class AspectRatio implements Comparable<AspectRatio> {
if (parts.length != 2) {
throw new NumberFormatException("Illegal AspectRatio string. Must be x:y");
}
int x = Integer.valueOf(parts[0]);
int y = Integer.valueOf(parts[1]);
int x = Integer.parseInt(parts[0]);
int y = Integer.parseInt(parts[1]);
return of(x, y);
}
@ -80,14 +80,11 @@ public class AspectRatio implements Comparable<AspectRatio> {
}
public boolean matches(@NonNull Size size) {
int gcd = gcd(size.getWidth(), size.getHeight());
int x = size.getWidth() / gcd;
int y = size.getHeight() / gcd;
return mX == x && mY == y;
return equals(AspectRatio.of(size));
}
public boolean matches(@NonNull Size size, float tolerance) {
return Math.abs(toFloat() - (float) size.getWidth() / size.getHeight()) <= tolerance;
return Math.abs(toFloat() - AspectRatio.of(size).toFloat()) <= tolerance;
}
@Override
@ -99,8 +96,7 @@ public class AspectRatio implements Comparable<AspectRatio> {
return true;
}
if (o instanceof AspectRatio) {
AspectRatio ratio = (AspectRatio) o;
return mX == ratio.mX && mY == ratio.mY;
return toFloat() == ((AspectRatio) o).toFloat();
}
return false;
}
@ -117,17 +113,12 @@ public class AspectRatio implements Comparable<AspectRatio> {
@Override
public int hashCode() {
return mY ^ ((mX << (Integer.SIZE / 2)) | (mX >>> (Integer.SIZE / 2)));
return Float.floatToIntBits(toFloat());
}
@Override
public int compareTo(@NonNull AspectRatio another) {
if (equals(another)) {
return 0;
} else if (toFloat() - another.toFloat() > 0) {
return 1;
}
return -1;
return Float.compare(toFloat(), another.toFloat());
}
/**
@ -140,6 +131,7 @@ public class AspectRatio implements Comparable<AspectRatio> {
return AspectRatio.of(mY, mX);
}
// Note: gcd(0,X) = gcd(X,0) = X (even for X=0)
private static int gcd(int a, int b) {
while (b != 0) {
int c = b;

@ -2,10 +2,12 @@ package com.otaliastudios.cameraview.video;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.internal.DeviceEncoders;
import com.otaliastudios.cameraview.internal.CamcorderProfiles;
@ -13,6 +15,7 @@ import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
/**
* A {@link VideoRecorder} that uses {@link android.media.MediaRecorder} APIs.
@ -105,6 +108,16 @@ public abstract class FullVideoRecorder extends VideoRecorder {
mProfile.videoCodec = MediaRecorder.VideoEncoder.H263;
mProfile.fileFormat = MediaRecorder.OutputFormat.MPEG_4; // should work
}
// Set audio codec if the user has specified a specific codec.
if (stub.audioCodec == AudioCodec.AAC) {
mProfile.audioCodec = MediaRecorder.AudioEncoder.AAC;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& stub.audioCodec == AudioCodec.HE_AAC) {
mProfile.audioCodec = MediaRecorder.AudioEncoder.HE_AAC;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& stub.audioCodec == AudioCodec.AAC_ELD) {
mProfile.audioCodec = MediaRecorder.AudioEncoder.AAC_ELD;
}
mMediaRecorder.setOutputFormat(mProfile.fileFormat);
// 4. Update the VideoResult stub with information from the profile, if the

@ -1,17 +1,20 @@
package com.otaliastudios.cameraview.video;
import android.graphics.SurfaceTexture;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.opengl.EGL14;
import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.internal.DeviceEncoders;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.overlay.OverlayDrawer;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread;
import com.otaliastudios.cameraview.filter.Filter;
@ -52,7 +55,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
private MediaEncoderEngine mEncoderEngine;
private final Object mEncoderEngineLock = new Object();
private GlCameraPreview mPreview;
private RendererCameraPreview mPreview;
private int mCurrentState = STATE_NOT_RECORDING;
private int mDesiredState = STATE_NOT_RECORDING;
@ -61,19 +64,16 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
private Overlay mOverlay;
private OverlayDrawer mOverlayDrawer;
private boolean mHasOverlay;
private int mOverlayRotation;
private Filter mCurrentFilter;
public SnapshotVideoRecorder(@NonNull CameraEngine engine,
@NonNull GlCameraPreview preview,
@Nullable Overlay overlay,
int overlayRotation) {
@NonNull RendererCameraPreview preview,
@Nullable Overlay overlay) {
super(engine);
mPreview = preview;
mOverlay = overlay;
mHasOverlay = overlay != null && overlay.drawsOn(Overlay.Target.VIDEO_SNAPSHOT);
mOverlayRotation = overlayRotation;
}
@Override
@ -126,9 +126,8 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
@RendererThread
@Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture,
float scaleX,
float scaleY) {
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, int rotation,
float scaleX, float scaleY) {
if (mCurrentState == STATE_NOT_RECORDING && mDesiredState == STATE_RECORDING) {
LOG.i("Starting the encoder engine.");
@ -145,7 +144,14 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
case H_264: videoType = "video/avc"; break; // MediaFormat.MIMETYPE_VIDEO_AVC:
case DEVICE_DEFAULT: videoType = "video/avc"; break;
}
String audioType = "audio/mp4a-latm";
String audioType = "";
switch (mResult.audioCodec) {
case AAC:
case HE_AAC:
case AAC_ELD: audioType = "audio/mp4a-latm"; break; // MediaFormat.MIMETYPE_AUDIO_AAC:
case DEVICE_DEFAULT: audioType = "audio/mp4a-latm"; break;
}
TextureConfig videoConfig = new TextureConfig();
AudioConfig audioConfig = new AudioConfig();
@ -219,7 +225,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
videoConfig.height = mResult.size.getHeight();
videoConfig.bitRate = mResult.videoBitRate;
videoConfig.frameRate = mResult.videoFrameRate;
videoConfig.rotation = mResult.rotation;
videoConfig.rotation = rotation + mResult.rotation;
videoConfig.mimeType = videoType;
videoConfig.encoder = deviceEncoders.getVideoEncoder();
videoConfig.textureId = mTextureId;
@ -232,13 +238,14 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
if (mHasOverlay) {
videoConfig.overlayTarget = Overlay.Target.VIDEO_SNAPSHOT;
videoConfig.overlayDrawer = mOverlayDrawer;
videoConfig.overlayRotation = mOverlayRotation;
videoConfig.overlayRotation = mResult.rotation;
// ^ no "rotation" here! Overlays are already in VIEW ref.
}
TextureMediaEncoder videoEncoder = new TextureMediaEncoder(videoConfig);
// Adjustment
mResult.rotation = 0; // We will rotate the result instead.
mCurrentFilter.setSize(mResult.size.getWidth(), mResult.size.getWidth());
mCurrentFilter.setSize(mResult.size.getWidth(), mResult.size.getHeight());
// Audio
AudioMediaEncoder audioEncoder = null;

@ -419,12 +419,14 @@ public abstract class MediaEncoder {
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// should happen before receiving buffers, and should only happen once
if (mController.isStarted()) {
throw new RuntimeException("MediaFormat changed twice.");
}
// throw new RuntimeException("MediaFormat changed twice.");
// Seen this happen in API31. TODO handle differently?
} else {
MediaFormat newFormat = mMediaCodec.getOutputFormat();
mTrackIndex = mController.notifyStarted(newFormat);
setState(STATE_STARTED);
mOutputBufferPool = new OutputBufferPool(mTrackIndex);
}
} else if (encoderStatus < 0) {
LOG.e("Unexpected result from dequeueOutputBuffer: " + encoderStatus);
// let's ignore it

@ -218,8 +218,8 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureConfig> {
mConfig.overlayDrawer.draw(mConfig.overlayTarget);
Matrix.translateM(mConfig.overlayDrawer.getTransform(),
0, 0.5F, 0.5F, 0);
Matrix.rotateM(mConfig.overlayDrawer.getTransform(), 0, mConfig.overlayRotation,
0, 0, 1);
Matrix.rotateM(mConfig.overlayDrawer.getTransform(),
0, mConfig.overlayRotation, 0, 0, 1);
Matrix.translateM(mConfig.overlayDrawer.getTransform(),
0, -0.5F, -0.5F, 0);
}

@ -40,36 +40,38 @@
<enum name="none" value="0" />
<enum name="autoFocus" value="1" />
<enum name="takePicture" value="2" />
<enum name="takePictureSnapshot" value="3" />
</attr>
<attr name="cameraGestureLongTap" format="enum">
<enum name="none" value="0" />
<enum name="autoFocus" value="1" />
<enum name="takePicture" value="2" />
<enum name="takePictureSnapshot" value="3" />
</attr>
<attr name="cameraGesturePinch" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="3" />
<enum name="exposureCorrection" value="4" />
<enum name="filterControl1" value="5" />
<enum name="filterControl2" value="6" />
<enum name="zoom" value="4" />
<enum name="exposureCorrection" value="5" />
<enum name="filterControl1" value="6" />
<enum name="filterControl2" value="7" />
</attr>
<attr name="cameraGestureScrollHorizontal" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="3" />
<enum name="exposureCorrection" value="4" />
<enum name="filterControl1" value="5" />
<enum name="filterControl2" value="6" />
<enum name="zoom" value="4" />
<enum name="exposureCorrection" value="5" />
<enum name="filterControl1" value="6" />
<enum name="filterControl2" value="7" />
</attr>
<attr name="cameraGestureScrollVertical" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="3" />
<enum name="exposureCorrection" value="4" />
<enum name="filterControl1" value="5" />
<enum name="filterControl2" value="6" />
<enum name="zoom" value="4" />
<enum name="exposureCorrection" value="5" />
<enum name="filterControl1" value="6" />
<enum name="filterControl2" value="7" />
</attr>
<attr name="cameraEngine" format="enum">
@ -141,6 +143,13 @@
<enum name="h264" value="2" />
</attr>
<attr name="cameraAudioCodec" format="enum">
<enum name="deviceDefault" value="0" />
<enum name="aac" value="1" />
<enum name="heAac" value="2" />
<enum name="aacEld" value="3" />
</attr>
<attr name="cameraAutoFocusResetDelay" format="integer|reference"/>
<attr name="cameraAutoFocusMarker" format="string|reference"/>
@ -160,6 +169,8 @@
<attr name="cameraRequestPermissions" format="boolean|reference"/>
<attr name="cameraExperimental" format="boolean|reference" />
<attr name="cameraDrawHardwareOverlays" format="boolean"/>
</declare-styleable>
<declare-styleable name="CameraView_Layout">

@ -1,27 +0,0 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.otaliastudios.cameraview.demo"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation project(':cameraview')
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0-beta01'
}

@ -0,0 +1,23 @@
plugins {
id("com.android.application")
id("kotlin-android")
}
android {
compileSdk = property("compileSdkVersion") as Int
defaultConfig {
applicationId = "com.otaliastudios.cameraview.demo"
minSdk = property("minSdkVersion") as Int
targetSdk = property("targetSdkVersion") as Int
versionCode = 1
versionName = "1.0"
vectorDrawables.useSupportLibrary = true
}
sourceSets["main"].java.srcDir("src/main/kotlin")
}
dependencies {
implementation(project(":cameraview"))
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("com.google.android.material:material:1.4.0")
}

@ -16,7 +16,8 @@
<activity
android:name=".CameraActivity"
android:configChanges="screenLayout|keyboardHidden"
android:hardwareAccelerated="true">
android:hardwareAccelerated="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

@ -1,417 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.animation.ValueAnimator;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.Toast;
import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraListener;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraView;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Preview;
import com.otaliastudios.cameraview.filter.Filters;
import com.otaliastudios.cameraview.filter.MultiFilter;
import com.otaliastudios.cameraview.filters.BrightnessFilter;
import com.otaliastudios.cameraview.filters.DuotoneFilter;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Arrays;
import java.util.List;
public class CameraActivity extends AppCompatActivity implements View.OnClickListener, OptionView.Callback {
private final static CameraLogger LOG = CameraLogger.create("DemoApp");
private final static boolean USE_FRAME_PROCESSOR = true;
private final static boolean DECODE_BITMAP = false;
private CameraView camera;
private ViewGroup controlPanel;
private long mCaptureTime;
private int mCurrentFilter = 0;
private final Filters[] mAllFilters = Filters.values();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE);
camera = findViewById(R.id.camera);
camera.setLifecycleOwner(this);
camera.addCameraListener(new Listener());
if (USE_FRAME_PROCESSOR) {
camera.addFrameProcessor(new FrameProcessor() {
private long lastTime = System.currentTimeMillis();
@Override
public void process(@NonNull Frame frame) {
long newTime = frame.getTime();
long delay = newTime - lastTime;
lastTime = newTime;
LOG.v("Frame delayMillis:", delay, "FPS:", 1000 / delay);
if (DECODE_BITMAP) {
if (frame.getFormat() == ImageFormat.NV21
&& frame.getDataClass() == byte[].class) {
byte[] data = frame.getData();
YuvImage yuvImage = new YuvImage(data,
frame.getFormat(),
frame.getSize().getWidth(),
frame.getSize().getHeight(),
null);
ByteArrayOutputStream jpegStream = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0,
frame.getSize().getWidth(),
frame.getSize().getHeight()), 100, jpegStream);
byte[] jpegByteArray = jpegStream.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray,
0, jpegByteArray.length);
//noinspection ResultOfMethodCallIgnored
bitmap.toString();
}
}
}
});
}
findViewById(R.id.edit).setOnClickListener(this);
findViewById(R.id.capturePicture).setOnClickListener(this);
findViewById(R.id.capturePictureSnapshot).setOnClickListener(this);
findViewById(R.id.captureVideo).setOnClickListener(this);
findViewById(R.id.captureVideoSnapshot).setOnClickListener(this);
findViewById(R.id.toggleCamera).setOnClickListener(this);
findViewById(R.id.changeFilter).setOnClickListener(this);
controlPanel = findViewById(R.id.controls);
ViewGroup group = (ViewGroup) controlPanel.getChildAt(0);
final View watermark = findViewById(R.id.watermark);
List<Option<?>> options = Arrays.asList(
// Layout
new Option.Width(), new Option.Height(),
// Engine and preview
new Option.Mode(), new Option.Engine(), new Option.Preview(),
// Some controls
new Option.Flash(), new Option.WhiteBalance(), new Option.Hdr(),
new Option.PictureMetering(), new Option.PictureSnapshotMetering(),
new Option.PictureFormat(),
// Video recording
new Option.PreviewFrameRate(), new Option.VideoCodec(), new Option.Audio(),
// Gestures
new Option.Pinch(), new Option.HorizontalScroll(), new Option.VerticalScroll(),
new Option.Tap(), new Option.LongTap(),
// Watermarks
new Option.OverlayInPreview(watermark),
new Option.OverlayInPictureSnapshot(watermark),
new Option.OverlayInVideoSnapshot(watermark),
// Frame Processing
new Option.FrameProcessingFormat(),
// Other
new Option.Grid(), new Option.GridColor(), new Option.UseDeviceOrientation()
);
List<Boolean> dividers = Arrays.asList(
// Layout
false, true,
// Engine and preview
false, false, true,
// Some controls
false, false, false, false, false, true,
// Video recording
false, false, true,
// Gestures
false, false, false, false, true,
// Watermarks
false, false, true,
// Frame Processing
true,
// Other
false, false, true
);
for (int i = 0; i < options.size(); i++) {
OptionView view = new OptionView(this);
//noinspection unchecked
view.setOption(options.get(i), this);
view.setHasDivider(dividers.get(i));
group.addView(view,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
controlPanel.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel);
b.setState(BottomSheetBehavior.STATE_HIDDEN);
}
});
// Animate the watermark just to show we record the animation in video snapshots
ValueAnimator animator = ValueAnimator.ofFloat(1F, 0.8F);
animator.setDuration(300);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float scale = (float) animation.getAnimatedValue();
watermark.setScaleX(scale);
watermark.setScaleY(scale);
watermark.setRotation(watermark.getRotation() + 2);
}
});
animator.start();
}
private void message(@NonNull String content, boolean important) {
if (important) {
LOG.w(content);
Toast.makeText(this, content, Toast.LENGTH_LONG).show();
} else {
LOG.i(content);
Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
}
}
private class Listener extends CameraListener {
@Override
public void onCameraOpened(@NonNull CameraOptions options) {
ViewGroup group = (ViewGroup) controlPanel.getChildAt(0);
for (int i = 0; i < group.getChildCount(); i++) {
OptionView view = (OptionView) group.getChildAt(i);
view.onCameraOpened(camera, options);
}
}
@Override
public void onCameraError(@NonNull CameraException exception) {
super.onCameraError(exception);
message("Got CameraException #" + exception.getReason(), true);
}
@Override
public void onPictureTaken(@NonNull PictureResult result) {
super.onPictureTaken(result);
if (camera.isTakingVideo()) {
message("Captured while taking video. Size=" + result.getSize(), false);
return;
}
// This can happen if picture was taken with a gesture.
long callbackTime = System.currentTimeMillis();
if (mCaptureTime == 0) mCaptureTime = callbackTime - 300;
LOG.w("onPictureTaken called! Launching activity. Delay:", callbackTime - mCaptureTime);
PicturePreviewActivity.setPictureResult(result);
Intent intent = new Intent(CameraActivity.this, PicturePreviewActivity.class);
intent.putExtra("delay", callbackTime - mCaptureTime);
startActivity(intent);
mCaptureTime = 0;
LOG.w("onPictureTaken called! Launched activity.");
}
@Override
public void onVideoTaken(@NonNull VideoResult result) {
super.onVideoTaken(result);
LOG.w("onVideoTaken called! Launching activity.");
VideoPreviewActivity.setVideoResult(result);
Intent intent = new Intent(CameraActivity.this, VideoPreviewActivity.class);
startActivity(intent);
LOG.w("onVideoTaken called! Launched activity.");
}
@Override
public void onVideoRecordingStart() {
super.onVideoRecordingStart();
LOG.w("onVideoRecordingStart!");
}
@Override
public void onVideoRecordingEnd() {
super.onVideoRecordingEnd();
message("Video taken. Processing...", false);
LOG.w("onVideoRecordingEnd!");
}
@Override
public void onExposureCorrectionChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) {
super.onExposureCorrectionChanged(newValue, bounds, fingers);
message("Exposure correction:" + newValue, false);
}
@Override
public void onZoomChanged(float newValue, @NonNull float[] bounds, @Nullable PointF[] fingers) {
super.onZoomChanged(newValue, bounds, fingers);
message("Zoom:" + newValue, false);
}
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.edit: edit(); break;
case R.id.capturePicture: capturePicture(); break;
case R.id.capturePictureSnapshot: capturePictureSnapshot(); break;
case R.id.captureVideo: captureVideo(); break;
case R.id.captureVideoSnapshot: captureVideoSnapshot(); break;
case R.id.toggleCamera: toggleCamera(); break;
case R.id.changeFilter: changeCurrentFilter(); break;
}
}
@Override
public void onBackPressed() {
BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel);
if (b.getState() != BottomSheetBehavior.STATE_HIDDEN) {
b.setState(BottomSheetBehavior.STATE_HIDDEN);
return;
}
super.onBackPressed();
}
private void edit() {
BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel);
b.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
private void capturePicture() {
if (camera.getMode() == Mode.VIDEO) {
message("Can't take HQ pictures while in VIDEO mode.", false);
return;
}
if (camera.isTakingPicture()) return;
mCaptureTime = System.currentTimeMillis();
message("Capturing picture...", false);
camera.takePicture();
}
private void capturePictureSnapshot() {
if (camera.isTakingPicture()) return;
if (camera.getPreview() != Preview.GL_SURFACE) {
message("Picture snapshots are only allowed with the GL_SURFACE preview.", true);
return;
}
mCaptureTime = System.currentTimeMillis();
message("Capturing picture snapshot...", false);
camera.takePictureSnapshot();
}
private void captureVideo() {
if (camera.getMode() == Mode.PICTURE) {
message("Can't record HQ videos while in PICTURE mode.", false);
return;
}
if (camera.isTakingPicture() || camera.isTakingVideo()) return;
message("Recording for 5 seconds...", true);
camera.takeVideo(new File(getFilesDir(), "video.mp4"), 5000);
}
private void captureVideoSnapshot() {
if (camera.isTakingVideo()) {
message("Already taking video.", false);
return;
}
if (camera.getPreview() != Preview.GL_SURFACE) {
message("Video snapshots are only allowed with the GL_SURFACE preview.", true);
return;
}
message("Recording snapshot for 5 seconds...", true);
camera.takeVideoSnapshot(new File(getFilesDir(), "video.mp4"), 5000);
}
private void toggleCamera() {
if (camera.isTakingPicture() || camera.isTakingVideo()) return;
switch (camera.toggleFacing()) {
case BACK:
message("Switched to back camera!", false);
break;
case FRONT:
message("Switched to front camera!", false);
break;
}
}
private void changeCurrentFilter() {
if (camera.getPreview() != Preview.GL_SURFACE) {
message("Filters are supported only when preview is Preview.GL_SURFACE.", true);
return;
}
if (mCurrentFilter < mAllFilters.length - 1) {
mCurrentFilter++;
} else {
mCurrentFilter = 0;
}
Filters filter = mAllFilters[mCurrentFilter];
message(filter.toString(), false);
// Normal behavior:
camera.setFilter(filter.newInstance());
// To test MultiFilter:
// DuotoneFilter duotone = new DuotoneFilter();
// duotone.setFirstColor(Color.RED);
// duotone.setSecondColor(Color.GREEN);
// camera.setFilter(new MultiFilter(duotone, filter.newInstance()));
}
@Override
public <T> boolean onValueChanged(@NonNull Option<T> option, @NonNull T value, @NonNull String name) {
if ((option instanceof Option.Width || option instanceof Option.Height)) {
Preview preview = camera.getPreview();
boolean wrapContent = (Integer) value == ViewGroup.LayoutParams.WRAP_CONTENT;
if (preview == Preview.SURFACE && !wrapContent) {
message("The SurfaceView preview does not support width or height changes. " +
"The view will act as WRAP_CONTENT by default.", true);
return false;
}
}
option.set(camera, value);
BottomSheetBehavior b = BottomSheetBehavior.from(controlPanel);
b.setState(BottomSheetBehavior.STATE_HIDDEN);
message("Changed " + option.getName() + " to " + name, false);
return true;
}
//region Permissions
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
boolean valid = true;
for (int grantResult : grantResults) {
valid = valid && grantResult == PackageManager.PERMISSION_GRANTED;
}
if (valid && !camera.isOpened()) {
camera.open();
}
}
//endregion
}

@ -1,46 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.content.Context;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
public class MessageView extends LinearLayout {
private TextView message;
private TextView title;
public MessageView(Context context) {
this(context, null);
}
public MessageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MessageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
inflate(context, R.layout.option_view, this);
ViewGroup content = findViewById(R.id.content);
inflate(context, R.layout.spinner_text, content);
title = findViewById(R.id.title);
message = (TextView) content.getChildAt(0);
}
public void setTitleAndMessage(String title, String message) {
setTitle(title);
setMessage(message);
}
public void setTitle(String title) {
this.title.setText(title);
}
public void setMessage(String message) {
this.message.setText(message);
}
}

@ -1,599 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import android.graphics.ImageFormat;
import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.CameraListener;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraView;
import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.gesture.Gesture;
import com.otaliastudios.cameraview.gesture.GestureAction;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.overlay.OverlayLayout;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Controls that we want to display in a ControlView.
*/
@SuppressWarnings("WeakerAccess")
public abstract class Option<T> {
private String name;
private Option(@NonNull String name) {
this.name = name;
}
@SuppressWarnings("WeakerAccess")
@NonNull
public final String getName() {
return name;
}
@NonNull
public abstract T get(@NonNull CameraView view);
@NonNull
public abstract Collection<T> getAll(@NonNull CameraView view, @NonNull CameraOptions options);
public abstract void set(@NonNull CameraView view, @NonNull T value);
@NonNull
public String toString(@NonNull T value) {
return String.valueOf(value).replace("_", " ").toLowerCase();
}
public static class Width extends Option<Integer> {
public Width() {
super("Width");
}
@NonNull
@Override
public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
View root = (View) view.getParent();
List<Integer> list = new ArrayList<>();
int boundary = root.getWidth();
if (boundary == 0) boundary = 1000;
int step = boundary / 10;
list.add(ViewGroup.LayoutParams.WRAP_CONTENT);
list.add(ViewGroup.LayoutParams.MATCH_PARENT);
for (int i = step; i < boundary; i += step) {
list.add(i);
}
return list;
}
@NonNull
@Override
public Integer get(@NonNull CameraView view) {
return view.getLayoutParams().width;
}
@Override
public void set(@NonNull CameraView view, @NonNull Integer value) {
view.getLayoutParams().width = (int) value;
view.setLayoutParams(view.getLayoutParams());
}
@NonNull
@Override
public String toString(@NonNull Integer value) {
if (value == ViewGroup.LayoutParams.MATCH_PARENT) return "match parent";
if (value == ViewGroup.LayoutParams.WRAP_CONTENT) return "wrap content";
return super.toString(value);
}
}
public static class Height extends Option<Integer> {
public Height() {
super("Height");
}
@NonNull
@Override
public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
View root = (View) view.getParent();
ArrayList<Integer> list = new ArrayList<>();
int boundary = root.getHeight();
if (boundary == 0) boundary = 1000;
int step = boundary / 10;
list.add(ViewGroup.LayoutParams.WRAP_CONTENT);
list.add(ViewGroup.LayoutParams.MATCH_PARENT);
for (int i = step; i < boundary; i += step) {
list.add(i);
}
return list;
}
@NonNull
@Override
public Integer get(@NonNull CameraView view) {
return view.getLayoutParams().height;
}
@Override
public void set(@NonNull CameraView view, @NonNull Integer value) {
view.getLayoutParams().height = (int) value;
view.setLayoutParams(view.getLayoutParams());
}
@NonNull
@Override
public String toString(@NonNull Integer value) {
if (value == ViewGroup.LayoutParams.MATCH_PARENT) return "match parent";
if (value == ViewGroup.LayoutParams.WRAP_CONTENT) return "wrap content";
return super.toString(value);
}
}
private static abstract class ControlOption<T extends Control> extends Option<T> {
private final Class<T> controlClass;
ControlOption(@NonNull Class<T> controlClass, String name) {
super(name);
this.controlClass = controlClass;
}
@Override
public void set(@NonNull CameraView view, @NonNull T value) {
view.set(value);
}
@NonNull
@Override
public T get(@NonNull CameraView view) {
return view.get(controlClass);
}
@NonNull
@Override
public Collection<T> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return options.getSupportedControls(controlClass);
}
}
public static class Mode extends ControlOption<com.otaliastudios.cameraview.controls.Mode> {
public Mode() {
super(com.otaliastudios.cameraview.controls.Mode.class, "Mode");
}
}
public static class Engine extends ControlOption<com.otaliastudios.cameraview.controls.Engine> {
public Engine() {
super(com.otaliastudios.cameraview.controls.Engine.class, "Engine");
}
@Override
public void set(final @NonNull CameraView view, final @NonNull com.otaliastudios.cameraview.controls.Engine value) {
boolean started = view.isOpened();
if (started) {
view.addCameraListener(new CameraListener() {
@Override
public void onCameraClosed() {
super.onCameraClosed();
view.removeCameraListener(this);
view.setEngine(value);
view.open();
}
});
view.close();
} else {
view.setEngine(value);
}
}
}
public static class Preview extends ControlOption<com.otaliastudios.cameraview.controls.Preview> {
public Preview() {
super(com.otaliastudios.cameraview.controls.Preview.class, "Preview Surface");
}
@Override
public void set(final @NonNull CameraView view, final @NonNull com.otaliastudios.cameraview.controls.Preview value) {
boolean opened = view.isOpened();
if (opened) {
view.addCameraListener(new CameraListener() {
@Override
public void onCameraClosed() {
super.onCameraClosed();
view.removeCameraListener(this);
applyPreview(view, value);
view.open();
}
});
view.close();
} else {
applyPreview(view, value);
}
}
// This is really tricky since the preview can only be changed when not attached to window.
private void applyPreview(@NonNull CameraView cameraView,
@NonNull com.otaliastudios.cameraview.controls.Preview newPreview) {
ViewGroup.LayoutParams params = cameraView.getLayoutParams();
ViewGroup parent = (ViewGroup) cameraView.getParent();
int index = 0;
for (int i = 0; i < parent.getChildCount(); i++) {
if (parent.getChildAt(i) == cameraView) {
index = i;
break;
}
}
parent.removeView(cameraView);
cameraView.setPreview(newPreview);
parent.addView(cameraView, index, params);
}
}
public static class Flash extends ControlOption<com.otaliastudios.cameraview.controls.Flash> {
public Flash() {
super(com.otaliastudios.cameraview.controls.Flash.class, "Flash");
}
}
public static class WhiteBalance extends ControlOption<com.otaliastudios.cameraview.controls.WhiteBalance> {
public WhiteBalance() {
super(com.otaliastudios.cameraview.controls.WhiteBalance.class, "White Balance");
}
}
public static class Hdr extends ControlOption<com.otaliastudios.cameraview.controls.Hdr> {
public Hdr() {
super(com.otaliastudios.cameraview.controls.Hdr.class, "HDR");
}
}
public static class PictureMetering extends Option<Boolean> {
public PictureMetering() {
super("Picture Metering");
}
@NonNull
@Override
public Boolean get(@NonNull CameraView view) {
return view.getPictureMetering();
}
@NonNull
@Override
public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return Arrays.asList(true, false);
}
@Override
public void set(@NonNull CameraView view, @NonNull Boolean value) {
view.setPictureMetering(value);
}
}
public static class PictureSnapshotMetering extends Option<Boolean> {
public PictureSnapshotMetering() {
super("Picture Snapshot Metering");
}
@NonNull
@Override
public Boolean get(@NonNull CameraView view) {
return view.getPictureSnapshotMetering();
}
@NonNull
@Override
public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return Arrays.asList(true, false);
}
@Override
public void set(@NonNull CameraView view, @NonNull Boolean value) {
view.setPictureSnapshotMetering(value);
}
}
public static class VideoCodec extends ControlOption<com.otaliastudios.cameraview.controls.VideoCodec> {
public VideoCodec() {
super(com.otaliastudios.cameraview.controls.VideoCodec.class, "Video Codec");
}
}
public static class Audio extends ControlOption<com.otaliastudios.cameraview.controls.Audio> {
public Audio() {
super(com.otaliastudios.cameraview.controls.Audio.class, "Audio");
}
}
private static abstract class GestureOption extends Option<GestureAction> {
private final Gesture gesture;
private final GestureAction[] allActions = GestureAction.values();
GestureOption(@NonNull Gesture gesture, String name) {
super(name);
this.gesture = gesture;
}
@Override
public void set(@NonNull CameraView view, @NonNull GestureAction value) {
view.mapGesture(gesture, value);
}
@NonNull
@Override
public GestureAction get(@NonNull CameraView view) {
return view.getGestureAction(gesture);
}
@NonNull
@Override
public Collection<GestureAction> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
List<GestureAction> list = new ArrayList<>();
for (GestureAction action : allActions) {
if (gesture.isAssignableTo(action) && options.supports(action)) {
list.add(action);
}
}
return list;
}
}
public static class Pinch extends GestureOption {
public Pinch() {
super(Gesture.PINCH, "Pinch");
}
}
public static class HorizontalScroll extends GestureOption {
public HorizontalScroll() {
super(Gesture.SCROLL_HORIZONTAL, "Horizontal Scroll");
}
}
public static class VerticalScroll extends GestureOption {
public VerticalScroll() {
super(Gesture.SCROLL_VERTICAL, "Vertical Scroll");
}
}
public static class Tap extends GestureOption {
public Tap() {
super(Gesture.TAP, "Tap");
}
}
public static class LongTap extends GestureOption {
public LongTap() {
super(Gesture.LONG_TAP, "Long Tap");
}
}
private static abstract class OverlayOption extends Option<Boolean> {
private View overlay;
private Overlay.Target target;
OverlayOption(@NonNull Overlay.Target target, @NonNull String name, @NonNull View overlay) {
super(name);
this.overlay = overlay;
this.target = target;
}
@NonNull
@Override
public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return Arrays.asList(true, false);
}
@NonNull
@Override
public Boolean get(@NonNull CameraView view) {
OverlayLayout.LayoutParams params = (OverlayLayout.LayoutParams) overlay.getLayoutParams();
switch (target) {
case PREVIEW: return params.drawOnPreview;
case PICTURE_SNAPSHOT: return params.drawOnPictureSnapshot;
case VIDEO_SNAPSHOT: return params.drawOnVideoSnapshot;
}
return false;
}
@Override
public void set(@NonNull CameraView view, @NonNull Boolean value) {
OverlayLayout.LayoutParams params = (OverlayLayout.LayoutParams) overlay.getLayoutParams();
switch (target) {
case PREVIEW: params.drawOnPreview = value; break;
case PICTURE_SNAPSHOT: params.drawOnPictureSnapshot = value; break;
case VIDEO_SNAPSHOT: params.drawOnVideoSnapshot = value; break;
}
overlay.setLayoutParams(params);
}
}
public static class OverlayInPreview extends OverlayOption {
public OverlayInPreview(@NonNull View overlay) {
super(Overlay.Target.PREVIEW, "Overlay in Preview", overlay);
}
}
public static class OverlayInPictureSnapshot extends OverlayOption {
public OverlayInPictureSnapshot(@NonNull View overlay) {
super(Overlay.Target.PICTURE_SNAPSHOT, "Overlay in Picture Snapshot", overlay);
}
}
public static class OverlayInVideoSnapshot extends OverlayOption {
public OverlayInVideoSnapshot(@NonNull View overlay) {
super(Overlay.Target.VIDEO_SNAPSHOT, "Overlay in Video Snapshot", overlay);
}
}
public static class Grid extends ControlOption<com.otaliastudios.cameraview.controls.Grid> {
public Grid() {
super(com.otaliastudios.cameraview.controls.Grid.class, "Grid Lines");
}
}
public static class GridColor extends Option<Pair<Integer, String>> {
public GridColor() {
super("Grid Color");
}
private static final List<Pair<Integer, String>> ALL = Arrays.asList(
new Pair<>(Color.argb(160, 255, 255, 255), "default"),
new Pair<>(Color.WHITE, "white"),
new Pair<>(Color.BLACK, "black"),
new Pair<>(Color.YELLOW, "yellow")
);
@NonNull
@Override
public Collection<Pair<Integer, String>> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return ALL;
}
@NonNull
@Override
public Pair<Integer, String> get(@NonNull CameraView view) {
for (Pair<Integer, String> pair : ALL) {
//noinspection ConstantConditions
if (pair.first == view.getGridColor()) {
return pair;
}
}
throw new RuntimeException("Could not find grid color");
}
@Override
public void set(@NonNull CameraView view, @NonNull Pair<Integer, String> value) {
//noinspection ConstantConditions
view.setGridColor(value.first);
}
@NonNull
@Override
public String toString(@NonNull Pair<Integer, String> value) {
//noinspection ConstantConditions
return value.second;
}
}
public static class UseDeviceOrientation extends Option<Boolean> {
public UseDeviceOrientation() {
super("Use Device Orientation");
}
@NonNull
@Override
public Collection<Boolean> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return Arrays.asList(true, false);
}
@NonNull
@Override
public Boolean get(@NonNull CameraView view) {
return view.getUseDeviceOrientation();
}
@Override
public void set(@NonNull CameraView view, @NonNull Boolean value) {
view.setUseDeviceOrientation(value);
}
}
public static class PreviewFrameRate extends Option<Integer> {
public PreviewFrameRate() {
super("Preview FPS");
}
@NonNull
@Override
public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
float min = options.getPreviewFrameRateMinValue();
float max = options.getPreviewFrameRateMaxValue();
float delta = max - min;
List<Integer> result = new ArrayList<>();
if (min == 0F && max == 0F) {
return result; // empty list
} else if (delta < 0.005F) {
result.add(Math.round(min));
return result; // single value
} else {
final int steps = 3;
final float step = delta / steps;
for (int i = 0; i <= steps; i++) {
result.add(Math.round(min));
min += step;
}
return result;
}
}
@NonNull
@Override
public Integer get(@NonNull CameraView view) {
return Math.round(view.getPreviewFrameRate());
}
@Override
public void set(@NonNull CameraView view, @NonNull Integer value) {
view.setPreviewFrameRate((float) value);
}
}
public static class PictureFormat extends ControlOption<com.otaliastudios.cameraview.controls.PictureFormat> {
public PictureFormat() {
super(com.otaliastudios.cameraview.controls.PictureFormat.class, "Picture Format");
}
}
public static class FrameProcessingFormat extends Option<Integer> {
FrameProcessingFormat() {
super("Frame Processing Format");
}
@Override
public void set(@NonNull CameraView view, @NonNull Integer value) {
view.setFrameProcessingFormat(value);
}
@NonNull
@Override
public Integer get(@NonNull CameraView view) {
return view.getFrameProcessingFormat();
}
@NonNull
@Override
public Collection<Integer> getAll(@NonNull CameraView view, @NonNull CameraOptions options) {
return options.getSupportedFrameProcessingFormats();
}
@NonNull
@Override
public String toString(@NonNull Integer value) {
switch (value) {
case ImageFormat.NV21: return "NV21";
case ImageFormat.NV16: return "NV16";
case ImageFormat.JPEG: return "JPEG";
case ImageFormat.YUY2: return "YUY2";
case ImageFormat.YUV_420_888: return "YUV_420_888";
case ImageFormat.YUV_422_888: return "YUV_422_888";
case ImageFormat.YUV_444_888: return "YUV_444_888";
case ImageFormat.RAW10: return "RAW10";
case ImageFormat.RAW12: return "RAW12";
case ImageFormat.RAW_SENSOR: return "RAW_SENSOR";
}
return super.toString(value);
}
}
}

@ -1,97 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.CameraView;
import java.util.ArrayList;
@SuppressLint("ViewConstructor")
public class OptionView<Value> extends LinearLayout implements Spinner.OnItemSelectedListener {
interface Callback {
<T> boolean onValueChanged(@NonNull Option<T> option, @NonNull T value, @NonNull String name);
}
private Value value;
private ArrayList<Value> values;
private ArrayList<String> valuesStrings;
private Option option;
private Callback callback;
private Spinner spinner;
public OptionView(@NonNull Context context) {
super(context);
setOrientation(VERTICAL);
inflate(context, R.layout.option_view, this);
ViewGroup content = findViewById(R.id.content);
spinner = new Spinner(context, Spinner.MODE_DROPDOWN);
content.addView(spinner);
}
public void setHasDivider(boolean hasDivider) {
View divider = findViewById(R.id.divider);
divider.setVisibility(hasDivider ? View.VISIBLE : View.GONE);
}
public void setOption(@NonNull Option<Value> option, @NonNull Callback callback) {
this.option = option;
this.callback = callback;
TextView title = findViewById(R.id.title);
title.setText(option.getName());
}
@SuppressWarnings("all")
public void onCameraOpened(CameraView view, CameraOptions options) {
values = new ArrayList(option.getAll(view, options));
value = (Value) option.get(view);
valuesStrings = new ArrayList<>();
for (Value value : values) {
valuesStrings.add(option.toString(value));
}
if (values.isEmpty()) {
spinner.setOnItemSelectedListener(null);
spinner.setEnabled(false);
spinner.setAlpha(0.8f);
spinner.setAdapter(new ArrayAdapter(getContext(),
R.layout.spinner_text, new String[]{ "Not supported." }));
spinner.setSelection(0, false);
} else {
spinner.setEnabled(true);
spinner.setAlpha(1f);
spinner.setAdapter(new ArrayAdapter(getContext(),
R.layout.spinner_text, valuesStrings));
spinner.setSelection(values.indexOf(value), false);
spinner.setOnItemSelectedListener(this);
}
}
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
if (!values.get(i).equals(value)) {
Log.e("ControlView", "curr: " + value + " new: " + values.get(i));
if (!callback.onValueChanged(option, values.get(i), valuesStrings.get(i))) {
spinner.setSelection(values.indexOf(value)); // Go back.
} else {
value = values.get(i);
}
}
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {}
}

@ -1,136 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.Toast;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.FileCallback;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.BitmapCallback;
import com.otaliastudios.cameraview.PictureResult;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class PicturePreviewActivity extends AppCompatActivity {
private static PictureResult picture;
public static void setPictureResult(@Nullable PictureResult pictureResult) {
picture = pictureResult;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_picture_preview);
final PictureResult result = picture;
if (result == null) {
finish();
return;
}
final ImageView imageView = findViewById(R.id.image);
final MessageView captureResolution = findViewById(R.id.nativeCaptureResolution);
final MessageView captureLatency = findViewById(R.id.captureLatency);
final MessageView exifRotation = findViewById(R.id.exifRotation);
final long delay = getIntent().getLongExtra("delay", 0);
AspectRatio ratio = AspectRatio.of(result.getSize());
captureLatency.setTitleAndMessage("Approx. latency", delay + " milliseconds");
captureResolution.setTitleAndMessage("Resolution", result.getSize() + " (" + ratio + ")");
exifRotation.setTitleAndMessage("EXIF rotation", result.getRotation() + "");
try {
result.toBitmap(1000, 1000, new BitmapCallback() {
@Override
public void onBitmapReady(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}
});
} catch (UnsupportedOperationException e) {
imageView.setImageDrawable(new ColorDrawable(Color.GREEN));
Toast.makeText(this, "Can't preview this format: " + picture.getFormat(),
Toast.LENGTH_LONG).show();
}
if (result.isSnapshot()) {
// Log the real size for debugging reason.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(result.getData(), 0, result.getData().length, options);
if (result.getRotation() % 180 != 0) {
Log.e("PicturePreview", "The picture full size is " + result.getSize().getHeight() + "x" + result.getSize().getWidth());
} else {
Log.e("PicturePreview", "The picture full size is " + result.getSize().getWidth() + "x" + result.getSize().getHeight());
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations()) {
setPictureResult(null);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.share, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.share) {
Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show();
String extension;
switch (picture.getFormat()) {
case JPEG: extension = "jpg"; break;
case DNG: extension = "dng"; break;
default: throw new RuntimeException("Unknown format.");
}
File file = new File(getFilesDir(), "picture." + extension);
CameraUtils.writeToFile(picture.getData(), file, new FileCallback() {
@Override
public void onFileReady(@Nullable File file) {
if (file != null) {
Context context = PicturePreviewActivity.this;
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("image/*");
Uri uri = FileProvider.getUriForFile(context,
context.getPackageName() + ".provider",
file);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
} else {
Toast.makeText(PicturePreviewActivity.this,
"Error while writing file.",
Toast.LENGTH_SHORT).show();
}
}
});
return true;
}
return super.onOptionsItemSelected(item);
}
}

@ -1,134 +0,0 @@
package com.otaliastudios.cameraview.demo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.MediaController;
import android.widget.Toast;
import android.widget.VideoView;
import com.otaliastudios.cameraview.CameraUtils;
import com.otaliastudios.cameraview.FileCallback;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.size.AspectRatio;
import java.io.File;
public class VideoPreviewActivity extends AppCompatActivity {
private VideoView videoView;
private static VideoResult videoResult;
public static void setVideoResult(@Nullable VideoResult result) {
videoResult = result;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_preview);
final VideoResult result = videoResult;
if (result == null) {
finish();
return;
}
videoView = findViewById(R.id.video);
videoView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
playVideo();
}
});
final MessageView actualResolution = findViewById(R.id.actualResolution);
final MessageView isSnapshot = findViewById(R.id.isSnapshot);
final MessageView rotation = findViewById(R.id.rotation);
final MessageView audio = findViewById(R.id.audio);
final MessageView audioBitRate = findViewById(R.id.audioBitRate);
final MessageView videoCodec = findViewById(R.id.videoCodec);
final MessageView videoBitRate = findViewById(R.id.videoBitRate);
final MessageView videoFrameRate = findViewById(R.id.videoFrameRate);
AspectRatio ratio = AspectRatio.of(result.getSize());
actualResolution.setTitleAndMessage("Size", result.getSize() + " (" + ratio + ")");
isSnapshot.setTitleAndMessage("Snapshot", result.isSnapshot() + "");
rotation.setTitleAndMessage("Rotation", result.getRotation() + "");
audio.setTitleAndMessage("Audio", result.getAudio().name());
audioBitRate.setTitleAndMessage("Audio bit rate", result.getAudioBitRate() + " bits per sec.");
videoCodec.setTitleAndMessage("VideoCodec", result.getVideoCodec().name());
videoBitRate.setTitleAndMessage("Video bit rate", result.getVideoBitRate() + " bits per sec.");
videoFrameRate.setTitleAndMessage("Video frame rate", result.getVideoFrameRate() + " fps");
MediaController controller = new MediaController(this);
controller.setAnchorView(videoView);
controller.setMediaPlayer(videoView);
videoView.setMediaController(controller);
videoView.setVideoURI(Uri.fromFile(result.getFile()));
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
ViewGroup.LayoutParams lp = videoView.getLayoutParams();
float videoWidth = mp.getVideoWidth();
float videoHeight = mp.getVideoHeight();
float viewWidth = videoView.getWidth();
lp.height = (int) (viewWidth * (videoHeight / videoWidth));
videoView.setLayoutParams(lp);
playVideo();
if (result.isSnapshot()) {
// Log the real size for debugging reason.
Log.e("VideoPreview", "The video full size is " + videoWidth + "x" + videoHeight);
}
}
});
}
void playVideo() {
if (!videoView.isPlaying()) {
videoView.start();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations()) {
setVideoResult(null);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.share, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.share) {
Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("video/*");
Uri uri = FileProvider.getUriForFile(this,
this.getPackageName() + ".provider",
videoResult.getFile());
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
}

@ -0,0 +1,344 @@
package com.otaliastudios.cameraview.demo
import android.animation.ValueAnimator
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.graphics.*
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.otaliastudios.cameraview.*
import com.otaliastudios.cameraview.controls.Facing
import com.otaliastudios.cameraview.controls.Mode
import com.otaliastudios.cameraview.controls.Preview
import com.otaliastudios.cameraview.filter.Filters
import com.otaliastudios.cameraview.frame.Frame
import com.otaliastudios.cameraview.frame.FrameProcessor
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.*
class CameraActivity : AppCompatActivity(), View.OnClickListener, OptionView.Callback {
companion object {
private val LOG = CameraLogger.create("DemoApp")
private const val USE_FRAME_PROCESSOR = false
private const val DECODE_BITMAP = false
}
private val camera: CameraView by lazy { findViewById(R.id.camera) }
private val controlPanel: ViewGroup by lazy { findViewById(R.id.controls) }
private var captureTime: Long = 0
private var currentFilter = 0
private val allFilters = Filters.values()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera)
CameraLogger.setLogLevel(CameraLogger.LEVEL_VERBOSE)
camera.setLifecycleOwner(this)
camera.addCameraListener(Listener())
if (USE_FRAME_PROCESSOR) {
camera.addFrameProcessor(object : FrameProcessor {
private var lastTime = System.currentTimeMillis()
override fun process(frame: Frame) {
val newTime = frame.time
val delay = newTime - lastTime
lastTime = newTime
LOG.v("Frame delayMillis:", delay, "FPS:", 1000 / delay)
if (DECODE_BITMAP) {
if (frame.format == ImageFormat.NV21
&& frame.dataClass == ByteArray::class.java) {
val data = frame.getData<ByteArray>()
val yuvImage = YuvImage(data,
frame.format,
frame.size.width,
frame.size.height,
null)
val jpegStream = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0,
frame.size.width,
frame.size.height), 100, jpegStream)
val jpegByteArray = jpegStream.toByteArray()
val bitmap = BitmapFactory.decodeByteArray(jpegByteArray,
0, jpegByteArray.size)
bitmap.toString()
}
}
}
})
}
findViewById<View>(R.id.edit).setOnClickListener(this)
findViewById<View>(R.id.capturePicture).setOnClickListener(this)
findViewById<View>(R.id.capturePictureSnapshot).setOnClickListener(this)
findViewById<View>(R.id.captureVideo).setOnClickListener(this)
findViewById<View>(R.id.captureVideoSnapshot).setOnClickListener(this)
findViewById<View>(R.id.toggleCamera).setOnClickListener(this)
findViewById<View>(R.id.changeFilter).setOnClickListener(this)
val group = controlPanel.getChildAt(0) as ViewGroup
val watermark = findViewById<View>(R.id.watermark)
val options: List<Option<*>> = listOf(
// Layout
Option.Width(), Option.Height(),
// Engine and preview
Option.Mode(), Option.Engine(), Option.Preview(),
// Some controls
Option.Flash(), Option.WhiteBalance(), Option.Hdr(),
Option.PictureMetering(), Option.PictureSnapshotMetering(),
Option.PictureFormat(),
// Video recording
Option.PreviewFrameRate(), Option.VideoCodec(), Option.Audio(), Option.AudioCodec(),
// Gestures
Option.Pinch(), Option.HorizontalScroll(), Option.VerticalScroll(),
Option.Tap(), Option.LongTap(),
// Watermarks
Option.OverlayInPreview(watermark),
Option.OverlayInPictureSnapshot(watermark),
Option.OverlayInVideoSnapshot(watermark),
// Frame Processing
Option.FrameProcessingFormat(),
// Other
Option.Grid(), Option.GridColor(), Option.UseDeviceOrientation()
)
val dividers = listOf(
// Layout
false, true,
// Engine and preview
false, false, true,
// Some controls
false, false, false, false, false, true,
// Video recording
false, false, false, true,
// Gestures
false, false, false, false, true,
// Watermarks
false, false, true,
// Frame Processing
true,
// Other
false, false, true
)
for (i in options.indices) {
val view = OptionView<Any>(this)
view.setOption(options[i] as Option<Any>, this)
view.setHasDivider(dividers[i])
group.addView(view, MATCH_PARENT, WRAP_CONTENT)
}
controlPanel.viewTreeObserver.addOnGlobalLayoutListener {
BottomSheetBehavior.from(controlPanel).state = BottomSheetBehavior.STATE_HIDDEN
}
// Animate the watermark just to show we record the animation in video snapshots
val animator = ValueAnimator.ofFloat(1f, 0.8f)
animator.duration = 300
animator.repeatCount = ValueAnimator.INFINITE
animator.repeatMode = ValueAnimator.REVERSE
animator.addUpdateListener { animation ->
val scale = animation.animatedValue as Float
watermark.scaleX = scale
watermark.scaleY = scale
watermark.rotation = watermark.rotation + 2
}
animator.start()
}
private fun message(content: String, important: Boolean) {
if (important) {
LOG.w(content)
Toast.makeText(this, content, Toast.LENGTH_LONG).show()
} else {
LOG.i(content)
Toast.makeText(this, content, Toast.LENGTH_SHORT).show()
}
}
private inner class Listener : CameraListener() {
override fun onCameraOpened(options: CameraOptions) {
val group = controlPanel.getChildAt(0) as ViewGroup
for (i in 0 until group.childCount) {
val view = group.getChildAt(i) as OptionView<*>
view.onCameraOpened(camera, options)
}
}
override fun onCameraError(exception: CameraException) {
super.onCameraError(exception)
message("Got CameraException #" + exception.reason, true)
}
override fun onPictureTaken(result: PictureResult) {
super.onPictureTaken(result)
if (camera.isTakingVideo) {
message("Captured while taking video. Size=" + result.size, false)
return
}
// This can happen if picture was taken with a gesture.
val callbackTime = System.currentTimeMillis()
if (captureTime == 0L) captureTime = callbackTime - 300
LOG.w("onPictureTaken called! Launching activity. Delay:", callbackTime - captureTime)
PicturePreviewActivity.pictureResult = result
val intent = Intent(this@CameraActivity, PicturePreviewActivity::class.java)
intent.putExtra("delay", callbackTime - captureTime)
startActivity(intent)
captureTime = 0
LOG.w("onPictureTaken called! Launched activity.")
}
override fun onVideoTaken(result: VideoResult) {
super.onVideoTaken(result)
LOG.w("onVideoTaken called! Launching activity.")
VideoPreviewActivity.videoResult = result
val intent = Intent(this@CameraActivity, VideoPreviewActivity::class.java)
startActivity(intent)
LOG.w("onVideoTaken called! Launched activity.")
}
override fun onVideoRecordingStart() {
super.onVideoRecordingStart()
LOG.w("onVideoRecordingStart!")
}
override fun onVideoRecordingEnd() {
super.onVideoRecordingEnd()
message("Video taken. Processing...", false)
LOG.w("onVideoRecordingEnd!")
}
override fun onExposureCorrectionChanged(newValue: Float, bounds: FloatArray, fingers: Array<PointF>?) {
super.onExposureCorrectionChanged(newValue, bounds, fingers)
message("Exposure correction:$newValue", false)
}
override fun onZoomChanged(newValue: Float, bounds: FloatArray, fingers: Array<PointF>?) {
super.onZoomChanged(newValue, bounds, fingers)
message("Zoom:$newValue", false)
}
}
override fun onClick(view: View) {
when (view.id) {
R.id.edit -> edit()
R.id.capturePicture -> capturePicture()
R.id.capturePictureSnapshot -> capturePictureSnapshot()
R.id.captureVideo -> captureVideo()
R.id.captureVideoSnapshot -> captureVideoSnapshot()
R.id.toggleCamera -> toggleCamera()
R.id.changeFilter -> changeCurrentFilter()
}
}
override fun onBackPressed() {
val b = BottomSheetBehavior.from(controlPanel)
if (b.state != BottomSheetBehavior.STATE_HIDDEN) {
b.state = BottomSheetBehavior.STATE_HIDDEN
return
}
super.onBackPressed()
}
private fun edit() {
BottomSheetBehavior.from(controlPanel).state = BottomSheetBehavior.STATE_COLLAPSED
}
private fun capturePicture() {
if (camera.mode == Mode.VIDEO) return run {
message("Can't take HQ pictures while in VIDEO mode.", false)
}
if (camera.isTakingPicture) return
captureTime = System.currentTimeMillis()
message("Capturing picture...", false)
camera.takePicture()
}
private fun capturePictureSnapshot() {
if (camera.isTakingPicture) return
if (camera.preview != Preview.GL_SURFACE) return run {
message("Picture snapshots are only allowed with the GL_SURFACE preview.", true)
}
captureTime = System.currentTimeMillis()
message("Capturing picture snapshot...", false)
camera.takePictureSnapshot()
}
private fun captureVideo() {
if (camera.mode == Mode.PICTURE) return run {
message("Can't record HQ videos while in PICTURE mode.", false)
}
if (camera.isTakingPicture || camera.isTakingVideo) return
message("Recording for 5 seconds...", true)
camera.takeVideo(File(filesDir, "video.mp4"), 5000)
}
private fun captureVideoSnapshot() {
if (camera.isTakingVideo) return run {
message("Already taking video.", false)
}
if (camera.preview != Preview.GL_SURFACE) return run {
message("Video snapshots are only allowed with the GL_SURFACE preview.", true)
}
message("Recording snapshot for 5 seconds...", true)
camera.takeVideoSnapshot(File(filesDir, "video.mp4"), 5000)
}
private fun toggleCamera() {
if (camera.isTakingPicture || camera.isTakingVideo) return
when (camera.toggleFacing()) {
Facing.BACK -> message("Switched to back camera!", false)
Facing.FRONT -> message("Switched to front camera!", false)
}
}
private fun changeCurrentFilter() {
if (camera.preview != Preview.GL_SURFACE) return run {
message("Filters are supported only when preview is Preview.GL_SURFACE.", true)
}
if (currentFilter < allFilters.size - 1) {
currentFilter++
} else {
currentFilter = 0
}
val filter = allFilters[currentFilter]
message(filter.toString(), false)
// Normal behavior:
camera.filter = filter.newInstance()
// To test MultiFilter:
// DuotoneFilter duotone = new DuotoneFilter();
// duotone.setFirstColor(Color.RED);
// duotone.setSecondColor(Color.GREEN);
// camera.setFilter(new MultiFilter(duotone, filter.newInstance()));
}
override fun <T : Any> onValueChanged(option: Option<T>, value: T, name: String): Boolean {
if (option is Option.Width || option is Option.Height) {
val preview = camera.preview
val wrapContent = value as Int == WRAP_CONTENT
if (preview == Preview.SURFACE && !wrapContent) {
message("The SurfaceView preview does not support width or height changes. " +
"The view will act as WRAP_CONTENT by default.", true)
return false
}
}
option.set(camera, value)
BottomSheetBehavior.from(controlPanel).state = BottomSheetBehavior.STATE_HIDDEN
message("Changed " + option.name + " to " + name, false)
return true
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
val valid = grantResults.all { it == PERMISSION_GRANTED }
if (valid && !camera.isOpened) {
camera.open()
}
}
}

@ -0,0 +1,34 @@
package com.otaliastudios.cameraview.demo
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
class MessageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
init {
orientation = VERTICAL
View.inflate(context, R.layout.option_view, this)
val content = findViewById<ViewGroup>(R.id.content)
View.inflate(context, R.layout.spinner_text, content)
}
private val message: TextView = findViewById<ViewGroup>(R.id.content).getChildAt(0) as TextView
private val title: TextView = findViewById(R.id.title)
fun setMessage(message: String) { this.message.text = message }
fun setTitle(title: String) { this.title.text = title }
fun setTitleAndMessage(title: String, message: String) {
setTitle(title)
setMessage(message)
}
}

@ -0,0 +1,296 @@
package com.otaliastudios.cameraview.demo
import android.graphics.Color
import android.graphics.ImageFormat
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.CameraOptions
import com.otaliastudios.cameraview.CameraView
import com.otaliastudios.cameraview.gesture.GestureAction
import com.otaliastudios.cameraview.overlay.OverlayLayout
import kotlin.math.roundToInt
import kotlin.reflect.KClass
/**
* Controls that we want to display in a ControlView.
*/
abstract class Option<T: Any>(val name: String) {
abstract fun get(view: CameraView): T
abstract fun getAll(view: CameraView, options: CameraOptions): Collection<T>
abstract fun set(view: CameraView, value: T)
open fun toString(value: T): String {
return "$value".replace("_", "_").toLowerCase()
}
class Width : Option<Int>("Width") {
override fun get(view: CameraView) = view.layoutParams.width
override fun set(view: CameraView, value: Int) {
view.layoutParams.width = value
view.layoutParams = view.layoutParams
}
override fun getAll(view: CameraView, options: CameraOptions): Collection<Int> {
val root = view.parent as View
val boundary = root.width.takeIf { it > 0 } ?: 1000
val step = boundary / 10
val list = mutableListOf(WRAP_CONTENT, MATCH_PARENT)
for (i in step until boundary step step) { list.add(i) }
return list
}
override fun toString(value: Int): String {
if (value == MATCH_PARENT) return "match parent"
if (value == WRAP_CONTENT) return "wrap content"
return super.toString(value)
}
}
class Height : Option<Int>("Height") {
override fun get(view: CameraView) = view.layoutParams.height
override fun set(view: CameraView, value: Int) {
view.layoutParams.height = value
view.layoutParams = view.layoutParams
}
override fun getAll(view: CameraView, options: CameraOptions): Collection<Int> {
val root = view.parent as View
val boundary = root.height.takeIf { it > 0 } ?: 1000
val step = boundary / 10
val list = mutableListOf(WRAP_CONTENT, MATCH_PARENT)
for (i in step until boundary step step) { list.add(i) }
return list
}
override fun toString(value: Int): String {
if (value == MATCH_PARENT) return "match parent"
if (value == WRAP_CONTENT) return "wrap content"
return super.toString(value)
}
}
abstract class Control<T: com.otaliastudios.cameraview.controls.Control>(private val kclass: KClass<T>, name: String) : Option<T>(name) {
override fun set(view: CameraView, value: T) = view.set(value)
override fun get(view: CameraView) = view.get(kclass.java)
override fun getAll(view: CameraView, options: CameraOptions)
= options.getSupportedControls(kclass.java)
}
class Mode : Control<com.otaliastudios.cameraview.controls.Mode>(com.otaliastudios.cameraview.controls.Mode::class, "Mode")
class Engine : Control<com.otaliastudios.cameraview.controls.Engine>(com.otaliastudios.cameraview.controls.Engine::class, "Engine") {
override fun set(view: CameraView, value: com.otaliastudios.cameraview.controls.Engine) {
if (view.isOpened) {
view.addCameraListener(object : CameraListener() {
override fun onCameraClosed() {
super.onCameraClosed()
view.removeCameraListener(this)
view.engine = value
view.open()
}
})
view.close()
} else {
view.engine = value
}
}
}
class Preview : Control<com.otaliastudios.cameraview.controls.Preview>(com.otaliastudios.cameraview.controls.Preview::class, "Preview Surface") {
override fun set(view: CameraView, value: com.otaliastudios.cameraview.controls.Preview) {
if (view.isOpened) {
view.addCameraListener(object : CameraListener() {
override fun onCameraClosed() {
super.onCameraClosed()
view.removeCameraListener(this)
applyPreview(view, value)
view.open()
}
})
view.close()
} else {
applyPreview(view, value)
}
}
// This is really tricky since the preview can only be changed when not attached to window.
private fun applyPreview(view: CameraView, value: com.otaliastudios.cameraview.controls.Preview) {
val params = view.layoutParams
val parent = view.parent as ViewGroup
val index = (0 until parent.childCount).first { parent.getChildAt(it) === view }
parent.removeView(view)
view.preview = value
parent.addView(view, index, params)
}
}
class Flash : Control<com.otaliastudios.cameraview.controls.Flash>(com.otaliastudios.cameraview.controls.Flash::class, "Flash")
class WhiteBalance : Control<com.otaliastudios.cameraview.controls.WhiteBalance>(com.otaliastudios.cameraview.controls.WhiteBalance::class, "White Balance")
class Hdr : Control<com.otaliastudios.cameraview.controls.Hdr>(com.otaliastudios.cameraview.controls.Hdr::class, "HDR")
class PictureMetering : Option<Boolean>("Picture Metering") {
override fun get(view: CameraView) = view.pictureMetering
override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false)
override fun set(view: CameraView, value: Boolean) { view.pictureMetering = value }
}
class PictureSnapshotMetering : Option<Boolean>("Picture Snapshot Metering") {
override fun get(view: CameraView) = view.pictureSnapshotMetering
override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false)
override fun set(view: CameraView, value: Boolean) { view.pictureSnapshotMetering = value }
}
class VideoCodec : Control<com.otaliastudios.cameraview.controls.VideoCodec>(com.otaliastudios.cameraview.controls.VideoCodec::class, "Video Codec")
class AudioCodec : Control<com.otaliastudios.cameraview.controls.AudioCodec>(com.otaliastudios.cameraview.controls.AudioCodec::class, "Audio Codec")
class Audio : Control<com.otaliastudios.cameraview.controls.Audio>(com.otaliastudios.cameraview.controls.Audio::class, "Audio")
abstract class Gesture(val gesture: com.otaliastudios.cameraview.gesture.Gesture, name: String) : Option<GestureAction>(name) {
override fun set(view: CameraView, value: GestureAction) {
view.mapGesture(gesture, value)
}
override fun get(view: CameraView): GestureAction {
return view.getGestureAction(gesture)
}
override fun getAll(view: CameraView, options: CameraOptions): Collection<GestureAction> {
return GestureAction.values().filter {
gesture.isAssignableTo(it) && options.supports(it)
}
}
}
class Pinch : Gesture(com.otaliastudios.cameraview.gesture.Gesture.PINCH, "Pinch")
class HorizontalScroll : Gesture(com.otaliastudios.cameraview.gesture.Gesture.SCROLL_HORIZONTAL, "Horizontal Scroll")
class VerticalScroll : Gesture(com.otaliastudios.cameraview.gesture.Gesture.SCROLL_VERTICAL, "Vertical Scroll")
class Tap : Gesture(com.otaliastudios.cameraview.gesture.Gesture.TAP, "Tap")
class LongTap : Gesture(com.otaliastudios.cameraview.gesture.Gesture.LONG_TAP, "Long Tap")
abstract class Overlay(private val overlay: View, private val target: com.otaliastudios.cameraview.overlay.Overlay.Target, name: String) : Option<Boolean>(name) {
override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false)
override fun get(view: CameraView): Boolean {
val params = overlay.layoutParams as OverlayLayout.LayoutParams
return when (target) {
com.otaliastudios.cameraview.overlay.Overlay.Target.PREVIEW -> params.drawOnPreview
com.otaliastudios.cameraview.overlay.Overlay.Target.PICTURE_SNAPSHOT -> params.drawOnPictureSnapshot
com.otaliastudios.cameraview.overlay.Overlay.Target.VIDEO_SNAPSHOT -> params.drawOnVideoSnapshot
}
}
override fun set(view: CameraView, value: Boolean) {
val params = overlay.layoutParams as OverlayLayout.LayoutParams
when (target) {
com.otaliastudios.cameraview.overlay.Overlay.Target.PREVIEW -> params.drawOnPreview = value
com.otaliastudios.cameraview.overlay.Overlay.Target.PICTURE_SNAPSHOT -> params.drawOnPictureSnapshot = value
com.otaliastudios.cameraview.overlay.Overlay.Target.VIDEO_SNAPSHOT -> params.drawOnVideoSnapshot = value
}
overlay.layoutParams = params
}
}
class OverlayInPreview(overlay: View) : Overlay(overlay, com.otaliastudios.cameraview.overlay.Overlay.Target.PREVIEW, "Overlay in Preview")
class OverlayInPictureSnapshot(overlay: View) : Overlay(overlay, com.otaliastudios.cameraview.overlay.Overlay.Target.PICTURE_SNAPSHOT, "Overlay in Picture Snapshot")
class OverlayInVideoSnapshot(overlay: View) : Overlay(overlay, com.otaliastudios.cameraview.overlay.Overlay.Target.VIDEO_SNAPSHOT, "Overlay in Video Snapshot")
class Grid : Control<com.otaliastudios.cameraview.controls.Grid>(com.otaliastudios.cameraview.controls.Grid::class, "Grid Lines")
class GridColor : Option<Pair<Int, String>>("Grid Color") {
private val all = listOf(
Color.argb(160, 255, 255, 255) to "default",
Color.WHITE to "white",
Color.BLACK to "black",
Color.YELLOW to "yellow"
)
override fun getAll(view: CameraView, options: CameraOptions) = all
override fun get(view: CameraView) = all.first { it.first == view.gridColor }
override fun set(view: CameraView, value: Pair<Int, String>) {
view.gridColor = value.first
}
}
class UseDeviceOrientation : Option<Boolean>("Use Device Orientation") {
override fun getAll(view: CameraView, options: CameraOptions) = listOf(true, false)
override fun get(view: CameraView) = view.useDeviceOrientation
override fun set(view: CameraView, value: Boolean) {
view.useDeviceOrientation = value
}
}
class PreviewFrameRate : Option<Int>("Preview FPS") {
override fun get(view: CameraView) = view.previewFrameRate.roundToInt()
override fun set(view: CameraView, value: Int) {
view.previewFrameRate = value.toFloat()
}
override fun getAll(view: CameraView, options: CameraOptions): Collection<Int> {
val min = options.previewFrameRateMinValue
val max = options.previewFrameRateMaxValue
val delta = max - min
return when {
min == 0F && max == 0F -> listOf()
delta < 0.005F -> listOf(min.roundToInt())
else -> {
val results = mutableListOf<Int>()
var value = min
for (i in 0 until 3) {
results.add(value.roundToInt())
value += delta / 3
}
results
}
}
}
}
class PictureFormat : Control<com.otaliastudios.cameraview.controls.PictureFormat>(com.otaliastudios.cameraview.controls.PictureFormat::class, "Picture Format")
class FrameProcessingFormat : Option<Int>("Frame Processing Format") {
override fun set(view: CameraView, value: Int) {
view.frameProcessingFormat = value
}
override fun get(view: CameraView) = view.frameProcessingFormat
override fun getAll(view: CameraView, options: CameraOptions)
= options.supportedFrameProcessingFormats
override fun toString(value: Int): String {
return when (value) {
ImageFormat.NV21 -> "NV21"
ImageFormat.NV16 -> "NV16"
ImageFormat.JPEG -> "JPEG"
ImageFormat.YUY2 -> "YUY2"
ImageFormat.YUV_420_888 -> "YUV_420_888"
ImageFormat.YUV_422_888 -> "YUV_422_888"
ImageFormat.YUV_444_888 -> "YUV_444_888"
ImageFormat.RAW10 -> "RAW10"
ImageFormat.RAW12 -> "RAW12"
ImageFormat.RAW_SENSOR -> "RAW_SENSOR"
else -> super.toString(value)
}
}
}
}

@ -0,0 +1,76 @@
package com.otaliastudios.cameraview.demo
import android.content.Context
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.*
import com.otaliastudios.cameraview.CameraOptions
import com.otaliastudios.cameraview.CameraView
class OptionView<Value: Any>(context: Context) : LinearLayout(context), AdapterView.OnItemSelectedListener {
interface Callback {
fun <T: Any> onValueChanged(option: Option<T>, value: T, name: String): Boolean
}
private lateinit var option: Option<Value>
private lateinit var callback: Callback
private lateinit var value: Value
private lateinit var values: List<Value>
private lateinit var valuesStrings: List<String>
init {
orientation = VERTICAL
View.inflate(context, R.layout.option_view, this)
}
val spinner = Spinner(context, Spinner.MODE_DROPDOWN).also {
val content = findViewById<ViewGroup>(R.id.content)
content.addView(it)
}
fun setHasDivider(hasDivider: Boolean) {
val divider = findViewById<View>(R.id.divider)
divider.visibility = if (hasDivider) View.VISIBLE else View.GONE
}
fun setOption(option: Option<Value>, callback: Callback) {
this.option = option
this.callback = callback
val title = findViewById<TextView>(R.id.title)
title.text = option.name
}
fun onCameraOpened(view: CameraView, options: CameraOptions) {
values = option.getAll(view, options).toList()
value = option.get(view)
valuesStrings = values.map { option.toString(it) }
if (values.isEmpty()) {
spinner.onItemSelectedListener = null
spinner.isEnabled = false
spinner.alpha = 0.8f
spinner.adapter = ArrayAdapter(context, R.layout.spinner_text, arrayOf("Not supported."))
spinner.setSelection(0, false)
} else {
spinner.isEnabled = true
spinner.alpha = 1f
spinner.adapter = ArrayAdapter(context, R.layout.spinner_text, valuesStrings)
spinner.setSelection(values.indexOf(value), false)
spinner.onItemSelectedListener = this
}
}
override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, i: Int, l: Long) {
if (values[i] != value) {
Log.e("ControlView", "curr: " + value + " new: " + values[i])
if (!callback.onValueChanged(option, values[i], valuesStrings[i])) {
spinner.setSelection(values.indexOf(value)) // Go back.
} else {
value = values[i]
}
}
}
override fun onNothingSelected(adapterView: AdapterView<*>?) {}
}

@ -0,0 +1,105 @@
package com.otaliastudios.cameraview.demo
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.otaliastudios.cameraview.CameraUtils
import com.otaliastudios.cameraview.PictureResult
import com.otaliastudios.cameraview.controls.PictureFormat
import com.otaliastudios.cameraview.size.AspectRatio
import java.io.File
class PicturePreviewActivity : AppCompatActivity() {
companion object {
var pictureResult: PictureResult? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_picture_preview)
val result = pictureResult ?: run {
finish()
return
}
val imageView = findViewById<ImageView>(R.id.image)
val captureResolution = findViewById<MessageView>(R.id.nativeCaptureResolution)
val captureLatency = findViewById<MessageView>(R.id.captureLatency)
val exifRotation = findViewById<MessageView>(R.id.exifRotation)
val delay = intent.getLongExtra("delay", 0)
val ratio = AspectRatio.of(result.size)
captureLatency.setTitleAndMessage("Approx. latency", "$delay milliseconds")
captureResolution.setTitleAndMessage("Resolution", "${result.size} ($ratio)")
exifRotation.setTitleAndMessage("EXIF rotation", result.rotation.toString())
try {
result.toBitmap(1000, 1000) { bitmap -> imageView.setImageBitmap(bitmap) }
} catch (e: UnsupportedOperationException) {
imageView.setImageDrawable(ColorDrawable(Color.GREEN))
Toast.makeText(this, "Can't preview this format: " + result.format, Toast.LENGTH_LONG).show()
}
if (result.isSnapshot) {
// Log the real size for debugging reason.
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeByteArray(result.data, 0, result.data.size, options)
if (result.rotation % 180 != 0) {
Log.e("PicturePreview", "The picture full size is ${result.size.height}x${result.size.width}")
} else {
Log.e("PicturePreview", "The picture full size is ${result.size.width}x${result.size.height}")
}
}
}
override fun onDestroy() {
super.onDestroy()
if (!isChangingConfigurations) {
pictureResult = null
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.share, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.share) {
Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show()
val extension = when (requireNotNull(pictureResult).format) {
PictureFormat.JPEG -> "jpg"
PictureFormat.DNG -> "dng"
else -> throw RuntimeException("Unknown format.")
}
val destFile = File(filesDir, "picture.$extension")
CameraUtils.writeToFile(requireNotNull(pictureResult?.data), destFile) { file ->
if (file != null) {
val context = this@PicturePreviewActivity
val intent = Intent(Intent.ACTION_SEND)
intent.type = "image/*"
val uri = FileProvider.getUriForFile(context,
context.packageName + ".provider", file)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
} else {
Toast.makeText(this@PicturePreviewActivity,
"Error while writing file.",
Toast.LENGTH_SHORT).show()
}
}
return true
}
return super.onOptionsItemSelected(item)
}
}

@ -0,0 +1,107 @@
package com.otaliastudios.cameraview.demo
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.MediaController
import android.widget.Toast
import android.widget.VideoView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.otaliastudios.cameraview.VideoResult
import com.otaliastudios.cameraview.size.AspectRatio
class VideoPreviewActivity : AppCompatActivity() {
companion object {
var videoResult: VideoResult? = null
}
private val videoView: VideoView by lazy { findViewById<VideoView>(R.id.video) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_preview)
val result = videoResult ?: run {
finish()
return
}
videoView.setOnClickListener { playVideo() }
val actualResolution = findViewById<MessageView>(R.id.actualResolution)
val isSnapshot = findViewById<MessageView>(R.id.isSnapshot)
val rotation = findViewById<MessageView>(R.id.rotation)
val audio = findViewById<MessageView>(R.id.audio)
val audioBitRate = findViewById<MessageView>(R.id.audioBitRate)
val videoCodec = findViewById<MessageView>(R.id.videoCodec)
val audioCodec = findViewById<MessageView>(R.id.audioCodec)
val videoBitRate = findViewById<MessageView>(R.id.videoBitRate)
val videoFrameRate = findViewById<MessageView>(R.id.videoFrameRate)
val ratio = AspectRatio.of(result.size)
actualResolution.setTitleAndMessage("Size", "${result.size} ($ratio)")
isSnapshot.setTitleAndMessage("Snapshot", result.isSnapshot.toString())
rotation.setTitleAndMessage("Rotation", result.rotation.toString())
audio.setTitleAndMessage("Audio", result.audio.name)
audioBitRate.setTitleAndMessage("Audio bit rate", "${result.audioBitRate} bits per sec.")
videoCodec.setTitleAndMessage("VideoCodec", result.videoCodec.name)
audioCodec.setTitleAndMessage("AudioCodec", result.audioCodec.name)
videoBitRate.setTitleAndMessage("Video bit rate", "${result.videoBitRate} bits per sec.")
videoFrameRate.setTitleAndMessage("Video frame rate", "${result.videoFrameRate} fps")
val controller = MediaController(this)
controller.setAnchorView(videoView)
controller.setMediaPlayer(videoView)
videoView.setMediaController(controller)
videoView.setVideoURI(Uri.fromFile(result.file))
videoView.setOnPreparedListener { mp ->
val lp = videoView.layoutParams
val videoWidth = mp.videoWidth.toFloat()
val videoHeight = mp.videoHeight.toFloat()
val viewWidth = videoView.width.toFloat()
lp.height = (viewWidth * (videoHeight / videoWidth)).toInt()
videoView.layoutParams = lp
playVideo()
if (result.isSnapshot) {
// Log the real size for debugging reason.
Log.e("VideoPreview", "The video full size is " + videoWidth + "x" + videoHeight)
}
}
}
fun playVideo() {
if (!videoView.isPlaying) {
videoView.start()
}
}
override fun onDestroy() {
super.onDestroy()
if (!isChangingConfigurations) {
videoResult = null
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.share, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.share) {
Toast.makeText(this, "Sharing...", Toast.LENGTH_SHORT).show()
val intent = Intent(Intent.ACTION_SEND)
intent.type = "video/*"
val uri = FileProvider.getUriForFile(this,
this.packageName + ".provider",
videoResult!!.file)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
return true
}
return super.onOptionsItemSelected(item)
}
}

@ -39,6 +39,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.otaliastudios.cameraview.demo.MessageView
android:id="@+id/audioCodec"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.otaliastudios.cameraview.demo.MessageView
android:id="@+id/videoCodec"
android:layout_width="match_parent"

1
docs/.gitignore vendored

@ -3,3 +3,4 @@ _pages
*.sw?
.sass-cache
.jekyll-metadata
Gemfile.lock

@ -1,248 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.2.10)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concurrent-ruby (1.1.4)
dnsruby (1.61.2)
addressable (~> 2.5)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
ethon (0.11.0)
ffi (>= 1.3.0)
eventmachine (1.2.7)
execjs (2.7.0)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
ffi (1.9.25)
forwardable-extended (2.6.0)
gemoji (3.0.0)
github-pages (193)
activesupport (= 4.2.10)
github-pages-health-check (= 1.8.1)
jekyll (= 3.7.4)
jekyll-avatar (= 0.6.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.5)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.11.0)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.9.4)
jekyll-mentions (= 1.4.1)
jekyll-optional-front-matter (= 0.3.0)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.2.0)
jekyll-redirect-from (= 0.14.0)
jekyll-relative-links (= 0.5.3)
jekyll-remote-theme (= 0.3.1)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.5.0)
jekyll-sitemap (= 1.2.0)
jekyll-swiss (= 0.4.0)
jekyll-theme-architect (= 0.1.1)
jekyll-theme-cayman (= 0.1.1)
jekyll-theme-dinky (= 0.1.1)
jekyll-theme-hacker (= 0.1.1)
jekyll-theme-leap-day (= 0.1.1)
jekyll-theme-merlot (= 0.1.1)
jekyll-theme-midnight (= 0.1.1)
jekyll-theme-minimal (= 0.1.1)
jekyll-theme-modernist (= 0.1.1)
jekyll-theme-primer (= 0.5.3)
jekyll-theme-slate (= 0.1.1)
jekyll-theme-tactile (= 0.1.1)
jekyll-theme-time-machine (= 0.1.1)
jekyll-titles-from-headings (= 0.5.1)
jemoji (= 0.10.1)
kramdown (= 1.17.0)
liquid (= 4.0.0)
listen (= 3.1.5)
mercenary (~> 0.3)
minima (= 2.5.0)
nokogiri (>= 1.8.2, < 2.0)
rouge (= 2.2.1)
terminal-table (~> 1.4)
github-pages-health-check (1.8.1)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (~> 4.0)
public_suffix (~> 2.0)
typhoeus (~> 1.3)
html-pipeline (2.9.1)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.6.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jekyll (3.7.4)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (~> 1.14)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
jekyll-avatar (0.6.0)
jekyll (~> 3.0)
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.2.0)
commonmarker (~> 0.14)
jekyll (>= 3.0, < 4.0)
jekyll-commonmark-ghpages (0.1.5)
commonmarker (~> 0.17.6)
jekyll-commonmark (~> 1)
rouge (~> 2)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.11.0)
jekyll (~> 3.3)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.9.4)
jekyll (~> 3.1)
octokit (~> 4.0, != 4.4.0)
jekyll-mentions (1.4.1)
html-pipeline (~> 2.3)
jekyll (~> 3.0)
jekyll-optional-front-matter (0.3.0)
jekyll (~> 3.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.2.0)
jekyll (~> 3.0)
jekyll-redirect-from (0.14.0)
jekyll (~> 3.3)
jekyll-relative-links (0.5.3)
jekyll (~> 3.3)
jekyll-remote-theme (0.3.1)
jekyll (~> 3.5)
rubyzip (>= 1.2.1, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.5.0)
jekyll (~> 3.3)
jekyll-sitemap (1.2.0)
jekyll (~> 3.3)
jekyll-swiss (0.4.0)
jekyll-theme-architect (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.5.3)
jekyll (~> 3.5)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.1)
jekyll (~> 3.3)
jekyll-watch (2.1.2)
listen (~> 3.0)
jemoji (0.10.1)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (~> 3.0)
kramdown (1.17.0)
liquid (4.0.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
mercenary (0.3.6)
mini_portile2 (2.4.0)
minima (2.5.0)
jekyll (~> 3.5)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.11.3)
multipart-post (2.0.0)
nokogiri (1.10.4)
mini_portile2 (~> 2.4.0)
octokit (4.13.0)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (2.0.5)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
rouge (2.2.1)
ruby-enum (0.7.2)
i18n
ruby_dep (1.5.0)
rubyzip (1.2.2)
safe_yaml (1.0.4)
sass (3.7.2)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (1.3.1)
ethon (>= 0.9.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unicode-display_width (1.4.0)
PLATFORMS
ruby
DEPENDENCIES
github-pages
BUNDLED WITH
1.17.2

@ -9,6 +9,58 @@ New versions are released through GitHub, so the reference page is the [GitHub R
> Starting from 2.4.0, you can now [support development](https://github.com/sponsors/natario1) through the GitHub Sponsors program.
Companies can share a tiny part of their revenue and get private support hours in return. Thanks!
##### v2.7.2
- Fix: fix camera rotation handling for Compose apps and other specific scenarios ([#1117][1117])
<https://github.com/natario1/CameraView/compare/v2.7.1...v2.7.2>
##### v2.7.1
- Fix: fix preview issues on Pixel 4A with certain FPS, thanks to [@honzasmuk][honzasmuk] ([#1089][1089])
- Improvement: don't catch gestures if they're turned off, thanks to [@ObsidianX][ObsidianX] ([#1068][1068])
- New: new flag cameraDrawHardwareOverlays and setDrawHardwareOverlays() to draw overlays on hardware canvas, thanks to [@ObsidianX][ObsidianX] ([#1066][1066])
- Publish on Maven Central instead of JCenter
<https://github.com/natario1/CameraView/compare/v2.7.0...v2.7.1>
##### v2.7.0
- New: onPictureShutter() callback when taking pictures, thanks to [@EzequielAdrianM][EzequielAdrianM] ([#1030][1030])
- New: GestureAction.TAKE_PICTURE_SNAPSHOT lets you take snapshots on gesture, thanks to [@EzequielAdrianM][EzequielAdrianM] ([#1030][1030])
- Improvement: try-catch internal exception when takePicture fails, thanks to [@michaelspecht][michaelspecht] ([#1024][1024])
- Improvement: log errors when file writing fails, thanks to [@bwt][bwt] ([#960][960])
- Fix: Avoid preview deadlocks ([#1020][1020])
- Fix: Workaround for messed-up preview on Pixel 4 ([#1020][1020])
- Fix: Avoid internal StackOverflow errors ([#992][992])
<https://github.com/natario1/CameraView/compare/v2.6.4...v2.7.0>
##### v2.6.4
- Fix: Fix many small bugs ([#953][953])
<https://github.com/natario1/CameraView/compare/v2.6.3...v2.6.4>
##### v2.6.3
- <small>[Video]</small> New: `setAudioCodec` and `app:cameraAudioCodec` to choose the audio encoding format, thanks to [@EverydayPineapple][EverydayPineapple] ([#861][861])
- <small>[Camera1, Frame processing]</small> Fix: frame processing restarts automatically after taking a picture, thanks to [@jeffreyfjohnson][jeffreyfjohnson] ([#877][877])
- <small>[Camera1]</small> Improvement: catch more errors in Camera1 lifecycle to avoid crashes, thanks to [@Namazed][Namazed] ([#851][851] and [#897][897])
- <small>[CameraView]</small> Improvement: `setLifecycleOwner` is now nullable and will unbind the lifecycle, thanks to [@Namazed][Namazed] ([#798][798])
- <small>[Preview]</small> Improvement: the `CameraPreview` APIs are much more friendly for subclassing ([#816][816])
<https://github.com/natario1/CameraView/compare/v2.6.2...v2.6.3>
##### v2.6.2
- <small>[Frame processing]</small> New: `frame.getRotationToUser()` and `frame.getRotationToView()` APIs to help with processing vs. rendering ([#745][745])
- <small>[Camera1, Camera2]</small> New: `cameraPreviewFrameRateExact="true|false"` to tell whether the desired preview frame rate should be as exact as possible, thanks to [@hualong-shen][hualong-shenn] ([#754][754])
- <small>[Logging]</small> Improvement: `CameraLogger` is now thread safe, thanks to [@Namazed][Namazed] ([#779][779])
- <small>[Permissions]</small> Improvement: added runtime API `setRequestPermissions()` that matches the XML attribute, thanks to [@Namazed][Namazed] ([#775][775])
<https://github.com/natario1/CameraView/compare/v2.6.1...v2.6.2>
##### v2.6.1
- <small>[Video]</small> New: `takeVideo(FileDescriptor)` for file descriptors, thanks to [@sewar][sewar] ([#732][732])
@ -297,6 +349,7 @@ This is the last release before v2.
[aartikov]: https://github.com/aartikov
[athornz]: https://github.com/athornz
[bwt]: https://github.com/bwt
[v-gar]: https://github.com/v-gar
[andrewmunn]: https://github.com/andrewmunn
[chaitanyaraghav]: https://github.com/chaitanyaraghav
@ -316,6 +369,13 @@ This is the last release before v2.
[RAN3000]: https://github.com/RAN3000
[vaibhavbhandula]: https://github.com/vaibhavbhandula
[sewar]: https://github.com/sewar
[hualong-shen]: https://github.com/hualong-shen
[EverydayPineapple]: https://github.com/EverydayPineapple
[jeffreyfjohnson]: https://github.com/jeffreyfjohnson
[michaelspecht]: https://github.com/michaelspecht
[EzequielAdrianM]: https://github.com/EzequielAdrianM
[honzasmuk]: https://github.com/honzasmuk
[ObsidianX]: https://github.com/ObsidianX
[73]: https://github.com/natario1/CameraView/pull/73
@ -403,3 +463,23 @@ This is the last release before v2.
[724]: https://github.com/natario1/CameraView/pull/724
[732]: https://github.com/natario1/CameraView/pull/732
[741]: https://github.com/natario1/CameraView/pull/741
[745]: https://github.com/natario1/CameraView/pull/745
[754]: https://github.com/natario1/CameraView/pull/754
[775]: https://github.com/natario1/CameraView/pull/775
[779]: https://github.com/natario1/CameraView/pull/779
[798]: https://github.com/natario1/CameraView/pull/798
[816]: https://github.com/natario1/CameraView/pull/816
[851]: https://github.com/natario1/CameraView/pull/851
[861]: https://github.com/natario1/CameraView/pull/861
[877]: https://github.com/natario1/CameraView/pull/877
[897]: https://github.com/natario1/CameraView/pull/897
[953]: https://github.com/natario1/CameraView/pull/953
[960]: https://github.com/natario1/CameraView/pull/960
[992]: https://github.com/natario1/CameraView/pull/992
[1020]: https://github.com/natario1/CameraView/pull/1020
[1024]: https://github.com/natario1/CameraView/pull/1024
[1030]: https://github.com/natario1/CameraView/pull/1030
[1089]: https://github.com/natario1/CameraView/pull/1089
[1068]: https://github.com/natario1/CameraView/pull/1068
[1066]: https://github.com/natario1/CameraView/pull/1066
[1117]: https://github.com/natario1/CameraView/pull/1117

@ -0,0 +1,95 @@
---
layout: page
title: "FAQs"
description: "Frequently asked questions"
order: 4
disqus: 1
---
### Usage
##### Q: Why is front camera flipped horizontally when using takePicture() or takeVideo() ?
A: It's actually not flipped - if you show your left hand, the person in the picture will show its left hand as well,
so this is the accurate representation of reality.
However, if you want to flip the result horizontally to match the preview,
you can do so by using the [snapshot APIs](../docs/capturing-media), which will respect what is shown in the preview.
##### Q: Can I use filters / overlays / cropping with takePicture() instead of takePictureSnapshot() ?
A: No, these features are only available with the snapshot API.
##### Q: Can I use filters / overlays / cropping with takeVideo() instead of takeVideoSnapshot() ?
A: No, these features are only available with the snapshot API.
##### Q: How can I improve takePictureSnapshot() quality?
A: The picture quality can be controlled in two ways:
- By [changing the snapshot size](../docs/snapshot-size)
- By [enabling metering](../docs/metering#picture-metering).
##### Q: How can I improve takeVideoSnapshot() quality?
A: The video quality can be controlled as follows:
- By changing the [snapshot size](../docs/snapshot-size)
- By changing the [snapshot framerate](../docs/controls#cameraPreviewFrameRate) (carefully: high values can cause dark preview)
- By changing the [video bitrate](../docs/controls#cameraVideoBitRate)
- By changing the [audio bitrate](../docs/controls#cameraAudioBitRate)
##### Q: How can I reduce the picture size?
A: The only control here is the picture size.
- When using `takePicture()`, change the [capture size](../docs/capture-size)
- When using `takePictureSnapshot()`, change the [snapshot size](../docs/snapshot-size)
##### Q: How can I reduce the video size?
A: By using video controls, for instance:
- Change the [video bitrate](../docs/controls#cameraVideoBitRate)
- Change the [audio bitrate](../docs/controls#cameraAudioBitRate)
- When using `takeVideo()`, change the [capture size](../docs/capture-size)
- When using `takeVideoSnapshot()`, change the [snapshot size](../docs/snapshot-size)
##### Q: Why is my preview / snapshot dark?
A: This is often caused by bad [framerate](../docs/controls#cameraPreviewFrameRate). Try using
a lower value, so that there's more time for frame exposure.
### Project Management
##### Q: I have found a bug with Camera1, can you fix it?
A: No, we will not address Camera1 bugs anymore - development is focused on Camera2. However, if you find a solution,
feel free to open a GitHub issue or pull requests to discuss.
##### Q: I have found a bug with my device XYZ, can you fix it?
A: No. Unless it's a device that we physically own, there is very little chance that a device-specific issues
can be solved by the maintainers. We encourage you to investigate on your own and get back to us
with a clear understanding of the problem and the solution.
##### Q: Why don't you review / comment on my GitHub issue?
A: Either because the issue did not respect the provided template, or because I don't have time.
If you are sure about the template, you can get private support by [sponsoring the project](../extra/donate).
##### Q: Why don't you review / comment on my GitHub pull requests?
A: Either because the pull requests did not respect the provided template, or because I don't have time.
If you are sure about the template, you can get private support by [sponsoring the project](../extra/donate).
##### Q: When will you do a new release?
A: We don't have a release schedule. New releases happen when there's enough changes to justify one,
and maintainers have had time to execute and publish the release. You can speed things up by
[sponsoring the project](../extra/donate) or pull snapshots from [jitpack.io](https://jitpack.io):
```groovy
implementation 'com.github.natario1:CameraView:main-SNAPSHOT'
implementation 'com.github.natario1:CameraView:<commit hash>'
```
Check their website for more information about how to set things up.

@ -7,21 +7,32 @@ order: 1
The library works on API 15+, which is the only requirement and should be met by most projects nowadays.
It is publicly hosted on [JCenter](https://bintray.com/natario/android/CameraView), where you
can download the AAR package. To fetch with Gradle, make sure you add the JCenter repository in your root projects `build.gradle` file:
```groovy
allprojects {
repositories {
jcenter()
}
It is publicly hosted on [Maven Central](https://repo.maven.apache.org/maven2/com/otaliastudios/cameraview), where you
can download the AAR package. To fetch with Gradle, make sure you add the Maven Central repository:
```kotlin
repositories {
mavenCentral()
}
```
Then simply download the latest version:
```groovy
api 'com.otaliastudios:cameraview:{{ site.github_version }}'
```kotlin
api("com.otaliastudios:cameraview:{{ site.github_version }}")
```
No other configuration steps are needed.
No other configuration steps are needed. If you want to try features that were not released yet,
you can pull the latest snapshot by adding the Sonatype snapshot repository:
```kotlin
repositories {
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
```
And depending on the latest-SNAPSHOT version:
```kotlin
api("com.otaliastudios:cameraview:latest-SNAPSHOT")
```

@ -12,8 +12,8 @@ google_analytics_id: 'UA-155077779-1'
google_site_verification: '4x49i17ABIrSvUl52SeL0-t0341aTnWWaC62-FYCRT4'
github: [metadata] # TODO What's this?
github_repo: CameraView
github_version: 2.6.1
github_branch: master
github_version: 2.7.2
github_branch: main
baseurl: '/CameraView' # Keep as an empty string if served up at the root
collections:
about:

@ -46,9 +46,9 @@ resulting snapshots are square as well, no matter what the sensor available size
|`takePictureSnapshot()`|Pictures|Snapshot|`yes`|`yes`|`yes`|That of the preview stream, [or less](snapshot-size)|
|`takeVideoSnapshot(File)`|Videos|Snapshot|`yes`|`yes`|`yes`|That of the preview stream, [or less](snapshot-size)|
> Please note that the video snaphot features requires:
> - API 18. If called before, it throws
> - An OpenGL preview (see [previews](previews)). If not, it throws
> Please note that the video snapshot features require:
> - API 18. If called on earlier versions, it throws an `IllegalStateException`
> - An OpenGL preview (see [previews](previews)). If not, it throws an `IllegalStateException`
### Capturing pictures while recording
@ -69,6 +69,11 @@ This is allowed at the following conditions:
```java
camera.addCameraListener(new CameraListener() {
@Override
public void onPictureShutter() {
// Picture capture started!
}
@Override
public void onPictureTaken(@NonNull PictureResult result) {
// A Picture was taken!

@ -70,6 +70,24 @@ cameraView.setVideoCodec(VideoCodec.H_263);
cameraView.setVideoCodec(VideoCodec.H_264);
```
##### cameraAudioCodec
Sets the audio encoder for video recordings. Defaults to `DEVICE_DEFAULT`,
which should typically be AAC.
The available values are exposed through the `CameraOptions` object.
`AudioCodec.HE_AAC` and `AudioCodec.AAC_ELD` require at least JellyBean.
The library will safely fall back to device default if the min API requirements
are not met.
```java
cameraView.setAudioCodec(AudioCodec.DEVICE_DEFAULT);
cameraView.setAudioCodec(AudioCodec.AAC);
cameraView.setAudioCodec(AudioCodec.HE_AAC);
cameraView.setAudioCodec(AudioCodec.AAC_ELD);
```
##### cameraWhiteBalance
Sets the desired white balance for the current session.

@ -25,7 +25,7 @@ flag to use it. The only condition is to use the `Preview.GL_SURFACE` preview.
Real-time filters are applied at creation time, through the `app:cameraFilter` XML attribute,
or anytime during the camera lifecycle using `cameraView.setFilter()`.
We offers a reasonable amount of filters through the `Filters` class, for example:
We offer a reasonable amount of filters through the `Filters` class, for example:
```java
cameraView.setFilter(Filters.BLACK_AND_WHITE.newInstance());

@ -29,7 +29,7 @@ cameraView.addFrameProcessor(new FrameProcessor() {
// Process android.media.Image...
}
}
}
});
```
For your convenience, the `FrameProcessor` method is run in a background thread so you can do your job
@ -120,7 +120,7 @@ You can check which formats are available for use through `CameraOptions.getSupp
### Advanced: Thread Control
Starting from `v2.5.1`, you can control the number of background threads that are allocated
for frame processing work. This should further push you into perform processing actions synchronously
for frame processing work. This should further push you into performing processing actions synchronously
and can be useful if processing is very slow with respect to the preview frame rate, in order to
avoid dropping too many frames.

@ -24,8 +24,8 @@ Simple as that. There are two things to be noted:
|Gesture|Description|Can be mapped to|
|-------------|-----------|----------------|
|`PINCH`|Pinch gesture, typically assigned to the zoom control.|`ZOOM` `EXPOSURE_CORRECTION` `FILTER_CONTROL_1` `FILTER_CONTROL_2` `NONE`|
|`TAP`|Single tap gesture, typically assigned to the focus control.|`AUTO_FOCUS` `TAKE_PICTURE` `NONE`|
|`LONG_TAP`|Long tap gesture.|`AUTO_FOCUS` `TAKE_PICTURE` `NONE`|
|`TAP`|Single tap gesture, typically assigned to the focus control.|`AUTO_FOCUS` `TAKE_PICTURE` `TAKE_PICTURE_SNAPSHOT` `NONE`|
|`LONG_TAP`|Long tap gesture.|`AUTO_FOCUS` `TAKE_PICTURE` `TAKE_PICTURE_SNAPSHOT` `NONE`|
|`SCROLL_HORIZONTAL`|Horizontal movement gesture.|`ZOOM` `EXPOSURE_CORRECTION` `FILTER_CONTROL_1` `FILTER_CONTROL_2` `NONE`|
|`SCROLL_VERTICAL`|Vertical movement gesture.|`ZOOM` `EXPOSURE_CORRECTION` `FILTER_CONTROL_1` `FILTER_CONTROL_2` `NONE`|
@ -38,6 +38,7 @@ Looking at this from the other side:
|`NONE`|Disables this gesture.|`TAP` `LONG_TAP` `PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`|
|`AUTO_FOCUS`|Launches a [touch metering operation](metering#touch-metering) on the finger position.|`TAP` `LONG_TAP`|
|`TAKE_PICTURE`|Takes a picture using [takePicture](capturing-media).|`TAP` `LONG_TAP`|
|`TAKE_PICTURE_SNAPSHOT`|Takes a picture using [takePictureSnapshot](capturing-media).|`TAP` `LONG_TAP`|
|`ZOOM`|[Zooms](controls#zoom) in or out.|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`|
|`EXPOSURE_CORRECTION`|Controls the [exposure correction](metering#exposure-correction).|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`|
|`FILTER_CONTROL_1`|Controls the first parameter (if any) of a [real-time filter](filters).|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`|
@ -48,8 +49,8 @@ Looking at this from the other side:
```xml
<com.otaliastudios.cameraview.CameraView
app:cameraGesturePinch="zoom|exposureCorrection|filterControl1|filterControl2|none"
app:cameraGestureTap="autoFocus|takePicture|none"
app:cameraGestureLongTap="autoFocus|takePicture|none"
app:cameraGestureTap="autoFocus|takePicture|takePictureSnapshot|none"
app:cameraGestureLongTap="autoFocus|takePicture|takePictureSnapshot|none"
app:cameraGestureScrollHorizontal="zoom|exposureCorrection|filterControl1|filterControl2|none"
app:cameraGestureScrollVertical="zoom|exposureCorrection|filterControl1|filterControl2|none"/>
```

@ -53,7 +53,7 @@ This action needs the coordinates of a point or region computed with respect to
```java
// Start touch metering at the center:
cameraView.startAutoFocus(cameraView.getWidth() / 2F, cameraView.getHeight/() / 2F);
cameraView.startAutoFocus(cameraView.getWidth() / 2F, cameraView.getHeight() / 2F);
// Start touch metering within a given area,
// like the bounding box of a face.
cameraView.startAutoFocus(rect);
@ -102,7 +102,7 @@ cameraView.setAutoFocusMarker(new DefaultAutoFocusMarker());
##### Touch Metering Reset Delay
You control control how a touch metering operation is reset after completed.
You control how a touch metering operation is reset after being completed.
Setting a negative value (or 0, or `Long.MAX_VALUE`) will not reset the metering values.
This is useful for low end devices that have slow auto-focus capabilities.
Defaults to 3 seconds.

@ -66,7 +66,7 @@ by the engine. The default selector will do the following:
- Constraint 2: match sizes a bit bigger than the View (so there is no upscaling)
- Try to match both, or just one, or fallback to the biggest available size
There are not so many reason why you would replace this, other than control the frame processor size
There are not so many reasons why you would replace this, other than to control the frame processor size
or, indirectly, the snapshot size. You can, however, hook into the process using `setPreviewStreamSize(SizeSelector)`:
```java
@ -79,7 +79,7 @@ cameraView.setPreviewStreamSize(new SizeSelector() {
});
```
After the preview stream size is determined, if it has changed since list time, the `CameraView` will receive
After the preview stream size is determined, if it has changed since last time, the `CameraView` will receive
another call to `onMeasure` so the `WRAP_CONTENT` magic can take place.
To understand how SizeSelectors work and the available utilities, please read the [Capture Size](capture-size) document.

@ -46,4 +46,4 @@ The automatic request is currently done at the activity level, so the permission
`onRequestPermissionResults()` will be invoked on the parent activity, not the fragment.
The automatic request can be disabled by setting `app:cameraRequestPermissions="false"` in your
XML declaration.
XML declaration or by using this method `setRequestPermissions(boolean requestPermissions)` in your code.

@ -45,8 +45,8 @@ Any changes in the overlay appearance will be recorded in real-time in the pictu
or video snapshot that you are capturing.
As you can see in the example, you can also selectively choose, for each overlay, whether it
will draw on the preview (`layout_drawOnPreview`), on picture snapshots (`layout_drawOnPreview`),
on video snapshots (`layout_drawOnPreview`).
will draw on the preview (`layout_drawOnPreview`), on picture snapshots (`layout_drawOnPictureSnapshot`),
on video snapshots (`layout_drawOnVideoSnapshot`).
### Advanced Usage
@ -82,3 +82,19 @@ params.drawOnVideoSnapshot = false; // do not draw on video snapshots
// When done, apply
overlay.setLayoutParams(params);
```
To capture a hardware rendered View such as a video rendered to a TextureView, enable the
`cameraDrawHardwareOverlays` flag:
```xml
<com.otaliastudios.cameraview.CameraView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cameraDrawHardwareOverlays="true"/>
```
Alternatively you can enable it in code with `setDrawHardwareOverlays()`:
```java
cameraView.setDrawHardwareOverlays(true);
```

@ -48,4 +48,4 @@ and [Google's CameraView](https://github.com/google/cameraview), but has been co
CameraKit's source code is licensed under the [MIT](https://github.com/wonderkiln/CameraKit-Android/blob/master/LICENSE) license.
CameraView is licensed under the [MIT](https://github.com/natario1/CameraView/blob/master/LICENSE) license as well.
CameraView is licensed under the [MIT](https://github.com/natario1/CameraView/blob/main/LICENSE) license as well.

@ -105,8 +105,8 @@ which means that **square videos** or any other ratio are possible.
The video snapshot supports audio and respects the `Audio`, max duration, max size & codec settings,
which makes it a powerful tool. The drawback is that it needs:
- API 18. If called before, it throws
- An OpenGL preview (see below). If not, it throws
- API 18. If called on earlier versions, it throws an `IllegalStateException`
- An OpenGL preview (see below). If not, it throws an `IllegalStateException`
##### Video capturing
Some new APIs were introduced, which are respected by both standard videos and snapshot videos:
@ -141,7 +141,7 @@ smart enough to
- respect the picture/video aspect ratio
- be a bit bigger than the view so that there is no upscaling
There are not so many reason why you would use this method, other than, for example, control the frame
There are not so many reasons why you would use this method, other than, for example, to control the frame
processor size or, indirectly, the snapshots size. If what you are doing is just assigning an aspect ratio,
for instance, please do so using `setPictureSize()` and `setVideoSize()`.

@ -5,6 +5,7 @@
<title>{{ site.title }}{% if page.title %} | {{ page.title }}{% endif %}</title>
<link rel="stylesheet" href="{{ site.baseurl }}/css/main.css">
<link rel="stylesheet" href="{{ site.baseurl }}/css/fonts_responsive.css">
<link rel="stylesheet" href="{{ site.baseurl }}/css/carbon.css">
</head>
<body>
{% include header.html %}

@ -2,10 +2,15 @@
layout: main
---
<div class="page-header border-bottom has-divider pb-3">
<div class="page-header border-bottom has-divider pb-3 d-flex flex-column flex-sm-row flex-nowrap align-items-start">
<div class="flex-grow-1">
<h1 class="d-inline-block mt-1 mb-0 mr-2">{{ page.title }}</h1>
{% if page.description %}<span class="mr-2">{{ page.description }}</span>{% endif %}
<span><a href="https://github.com/{{ site.author.github }}/{{ site.github_repo }}/edit/{{ site.github_branch }}/docs/{{ page.path }}">[edit this page]</a></span>
{% if page.description %}<div><span class="mr-2">{{ page.description }}</span></div>{% endif %}
<div><span><a href="https://github.com/{{ site.author.github }}/{{ site.github_repo }}/edit/{{ site.github_branch }}/docs/{{ page.path }}">[edit this page]</a></span></div>
</div>
<div class="flex-shrink-0 pt-2 pl-sm-2">
<script async type="text/javascript" src="//cdn.carbonads.com/carbon.js?serve=CE7DL27Y&placement=natario1githubio" id="_carbonads_js"></script>
</div>
</div>
<div class="page my-4">
{{ content }}

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

Loading…
Cancel
Save