Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: iglaweb/DuoCamera
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.0.0
Choose a base ref
...
head repository: iglaweb/DuoCamera
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 7 commits
  • 31 files changed
  • 1 contributor

Commits on Oct 17, 2021

  1. Refactored;

    Fixes;
    iglaweb committed Oct 17, 2021
    Copy the full SHA
    521d993 View commit details

Commits on Oct 31, 2021

  1. Copy the full SHA
    9ccde31 View commit details
  2. Copy the full SHA
    416cc49 View commit details
  3. Fix naming;

    iglaweb committed Oct 31, 2021
    Copy the full SHA
    6808d29 View commit details

Commits on Nov 10, 2021

  1. Copy the full SHA
    ee73a23 View commit details

Commits on Nov 11, 2021

  1. Refactored;

    iglaweb committed Nov 11, 2021
    Copy the full SHA
    e8b1e0e View commit details
  2. Add import;

    iglaweb committed Nov 11, 2021
    Copy the full SHA
    5743d5c View commit details
Showing with 1,087 additions and 908 deletions.
  1. +1 −1 app/src/main/AndroidManifest.xml
  2. +384 −0 app/src/main/java/ru/igla/duocamera/core/CameraProvider.kt
  3. +13 −0 app/src/main/java/ru/igla/duocamera/core/CameraStateListener.kt
  4. +1 −1 app/src/main/java/ru/igla/duocamera/{ui → core}/DuoCameraApp.kt
  5. +28 −0 app/src/main/java/ru/igla/duocamera/core/FpsCalculator.kt
  6. +26 −18 app/src/main/java/ru/igla/duocamera/{ui → core}/MediaRecorderWrapper.kt
  7. +0 −1 app/src/main/java/ru/igla/duocamera/dto/CameraInfo.kt
  8. +1 −1 app/src/main/java/ru/igla/duocamera/{utils → dto}/CameraReqType.kt
  9. +6 −6 app/src/main/java/ru/igla/duocamera/ui/DebugCameraActivity.kt
  10. +108 −369 app/src/main/java/ru/igla/duocamera/ui/DebugCameraFragment.kt
  11. +6 −4 app/src/main/java/ru/igla/duocamera/ui/PermissionsFragment.kt
  12. +5 −1 app/src/main/java/ru/igla/duocamera/ui/SelectorFragment.kt
  13. +0 −12 app/src/main/java/ru/igla/duocamera/ui/toastcompat/BadTokenListener.java
  14. +10 −0 app/src/main/java/ru/igla/duocamera/ui/toastcompat/BadTokenListener.kt
  15. +0 −111 app/src/main/java/ru/igla/duocamera/ui/toastcompat/SafeToastContext.java
  16. +71 −0 app/src/main/java/ru/igla/duocamera/ui/toastcompat/SafeToastContext.kt
  17. +0 −180 app/src/main/java/ru/igla/duocamera/ui/toastcompat/ToastCompat.java
  18. +130 −0 app/src/main/java/ru/igla/duocamera/ui/toastcompat/ToastCompat.kt
  19. +0 −96 app/src/main/java/ru/igla/duocamera/ui/toastcompat/Toaster.java
  20. +73 −0 app/src/main/java/ru/igla/duocamera/ui/toastcompat/Toaster.kt
  21. +1 −1 app/src/main/java/ru/igla/duocamera/utils/CameraSizes.kt
  22. +1 −0 app/src/main/java/ru/igla/duocamera/utils/CameraUtils.kt
  23. +0 −1 app/src/main/java/ru/igla/duocamera/utils/Constants.kt
  24. +13 −59 app/src/main/java/ru/igla/duocamera/utils/ExtensionUtils.kt
  25. +1 −1 app/src/main/java/ru/igla/duocamera/utils/FpsMeasure.kt
  26. +80 −0 app/src/main/java/ru/igla/duocamera/utils/TensorFlowImageUtils.java
  27. +0 −30 app/src/main/java/ru/igla/duocamera/utils/ViewUtils.kt
  28. +95 −0 app/src/main/java/ru/igla/duocamera/utils/YubToBitmapConverter.kt
  29. +0 −3 app/src/main/res/layout/debug_activity_camera.xml
  30. +33 −0 app/src/main/res/layout/debug_camera_fragment.xml
  31. +0 −12 app/src/main/res/values/attrs.xml
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@
android:xlargeScreens="true" />

<application
android:name=".ui.DuoCameraApp"
android:name=".core.DuoCameraApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
384 changes: 384 additions & 0 deletions app/src/main/java/ru/igla/duocamera/core/CameraProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
package ru.igla.duocamera.core

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageFormat
import android.hardware.camera2.*
import android.media.ImageReader
import android.media.MediaScannerConnection
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.util.Range
import android.util.Size
import android.view.*
import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.*
import ru.igla.duocamera.dto.CameraInfoExt
import ru.igla.duocamera.dto.CameraReqType
import ru.igla.duocamera.ui.DebugCameraActivity
import ru.igla.duocamera.ui.widgets.AutoFitSurfaceView
import ru.igla.duocamera.utils.*
import java.io.File
import java.util.*
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

class CameraProvider(
val context: Context,
val cameraStateListener: CameraStateListener,
private val lifecycleScope: LifecycleCoroutineScope,
private val viewFinder: AutoFitSurfaceView,
private val cameraInfoExt: CameraInfoExt,
private val recordingListener: RecordingListener,
private val readBitmapListener: ReadBitmapListener
) {

interface RecordingListener {
suspend fun onStartRecording()
suspend fun onStopRecording(outputFile: File)
}

interface ReadBitmapListener {
fun onReadBitmap(bitmap: Bitmap)
}


private var recordingStatus = STATE_IDLE

/**
* An [ImageReader] that handles live preview.
*/
private var imageReaderPreview: ImageReader? = null


/** Detects, characterizes, and connects to a CameraDevice (used for all camera operations) */
private val cameraManager: CameraManager by lazy {
val context = context.applicationContext
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
}

/** [CameraCharacteristics] corresponding to the provided Camera ID */
private val characteristics: CameraCharacteristics by lazy {
cameraManager.getCameraCharacteristics(cameraInfoExt.cameraRequestId)
}

/**
* Setup a persistent [Surface] for the recorder so we can use it as an output target for the
* camera session without preparing the recorder
*/
private val recorderSurface: Surface by lazy {
mediaRecorderWrapper.createPersistentSurface()
}

private val mediaRecorderWrapper by lazy {
MediaRecorderWrapper(context.applicationContext)
}

/** [HandlerThread] where all camera operations run */
private val cameraThread = HandlerThread("CameraThread").apply { start() }

/** [Handler] corresponding to [cameraThread] */
private val cameraHandler = Handler(cameraThread.looper)

private val recordBgThread by lazy {
Executors.newSingleThreadExecutor().asCoroutineDispatcher()
}


/** Captures frames from a [CameraDevice] for our video recording */
private lateinit var session: CameraCaptureSession

/** The [CameraDevice] that will be opened in this fragment */
private lateinit var camera: CameraDevice

/** Requests used for preview only in the [CameraCaptureSession] */
private val previewRequest: CaptureRequest by lazy {
// Capture request holds references to target surfaces
session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
// Add the preview surface target
addTarget(viewFinder.holder.surface)
imageReaderPreview?.let {
addTarget(it.surface)
}
}.build()
}

/** Requests used for preview and recording in the [CameraCaptureSession] */
private val recordRequest: CaptureRequest by lazy {
// Capture request holds references to target surfaces
session.device.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
// Add the preview and recording surface targets
addTarget(viewFinder.holder.surface)
addTarget(recorderSurface)
imageReaderPreview?.apply {
addTarget(surface)
}
// Sets user requested FPS for all targets
set(
CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
Range(cameraInfoExt.cameraInfo.fps, cameraInfoExt.cameraInfo.fps)
)
}.build()
}

private val yubToBitmapConverter by lazy { YubToBitmapConverter() }

/**
* The [android.util.Size] of camera preview.
*/
lateinit var previewSize: Size


/** Live data listener for changes in the device orientation relative to the camera */
lateinit var relativeOrientation: OrientationLiveData

@SuppressLint("MissingPermission")
fun initCameraLayout() {
viewFinder.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) = Unit

override fun surfaceCreated(holder: SurfaceHolder) {
// Selects appropriate preview size and configures view finder
val previewSize = fetchPreviewSize()
this@CameraProvider.previewSize = previewSize

logD {
"View finder size: ${viewFinder.width} x ${viewFinder.height}"
}
logD { "Selected preview size: $previewSize" }
viewFinder.setAspectRatio(
previewSize.width,
previewSize.height
)

// To ensure that size is set, initialize camera in the view's thread
viewFinder.post { initializeCamera() }
}
})

// Used to rotate the output media to match device orientation
relativeOrientation = OrientationLiveData(context, characteristics)
}

private fun fetchPreviewSize(): Size {
when (cameraInfoExt.cameraInfo.cameraReqType) {
CameraReqType.GENERAL_CAMERA_SIZE -> {
return cameraInfoExt.cameraInfo.size
}
CameraReqType.REQ_MIN_SIZE -> {
return getMinPreviewOutputSize(
characteristics,
SurfaceHolder::class.java
)
}
else -> {
return getMaxPreviewOutputSize(
viewFinder.display,
characteristics,
SurfaceHolder::class.java
)
}
}
}

private suspend fun requestStartRecord() {
// Start recording repeating requests, which will stop the ongoing preview
// repeating requests without having to explicitly call `session.stopRepeating`
session.setRepeatingRequest(recordRequest, null, cameraHandler)

// Finalizes recorder setup and starts recording
mediaRecorderWrapper.recorder = mediaRecorderWrapper.createRecorder(recorderSurface).apply {
// Sets output orientation based on current sensor value at start time
relativeOrientation.value?.let { setOrientationHint(it) }

mediaRecorderWrapper.startRecording(this)
}

recordingListener.onStartRecording()
}

private suspend fun requestStopRecording(view: View) {
mediaRecorderWrapper.stopRecording()

// Broadcasts the media file to the rest of the system
logI { "Scan video file ${mediaRecorderWrapper.outputFile.absolutePath}" }
MediaScannerConnection.scanFile(
view.context,
arrayOf(mediaRecorderWrapper.outputFile.absolutePath),
null,
null
)

recordingListener.onStopRecording(mediaRecorderWrapper.outputFile)

// Finishes our current camera screen
delay(DebugCameraActivity.ANIMATION_SLOW_MILLIS)
}

private val previewAvailableListener =
ImageReader.OnImageAvailableListener { reader ->
val image = reader.acquireLatestImage() ?: return@OnImageAvailableListener

val bitmap = yubToBitmapConverter.extractBitmap(image, previewSize)
readBitmapListener.onReadBitmap(bitmap)
image.close()
}

/**
* Begin all camera operations in a coroutine in the main thread. This function:
* - Opens the camera
* - Configures the camera session
* - Starts the preview by dispatching a repeating request
*/
@SuppressLint("ClickableViewAccessibility")
private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) {
// Open the selected camera
camera = openCamera(cameraManager, cameraInfoExt.cameraRequestId, cameraHandler)

val imageReader = ImageReader.newInstance(
imageRetrieveSize.width,
imageRetrieveSize.height,
ImageFormat.YUV_420_888,
1
).apply {
setOnImageAvailableListener(
previewAvailableListener,
null
)
}.also {
imageReaderPreview = it
}

// Creates list of Surfaces where the camera will output frames
val targets = listOf(
viewFinder.holder.surface,
imageReader.surface,
recorderSurface
)

// Start a capture session using our open camera and list of Surfaces where frames will go
session = createCaptureSession(camera, targets, cameraHandler)

// Sends the capture request as frequently as possible until the session is torn down or
// session.stopRepeating() is called
session.setRepeatingRequest(previewRequest, null, cameraHandler)

cameraStateListener.onInitCamera(camera)
}

fun toggleRecord(view: View) {
lifecycleScope.launch(recordBgThread) {
if (recordingStatus == STATE_IDLE) {
recordingStatus = STATE_STARTING
requestStartRecord()
recordingStatus = STATE_RECORDING
} else if (recordingStatus == STATE_RECORDING) {
recordingStatus = STATE_STOPPING
requestStopRecording(view)
recordingStatus = STATE_IDLE
}
}
}

/** Opens the camera and returns the opened device (as the result of the suspend coroutine) */
@SuppressLint("MissingPermission")
private suspend fun openCamera(
manager: CameraManager,
cameraId: String,
handler: Handler? = null
): CameraDevice = suspendCancellableCoroutine { cont ->
manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) {
cameraStateListener.onOpened(device)
cont.resume(device)
}

override fun onDisconnected(device: CameraDevice) {
logI { "Camera $cameraId has been disconnected" }
cameraStateListener.onDisconnected(device)
}

override fun onError(device: CameraDevice, error: Int) {
val msg = when (error) {
ERROR_CAMERA_DEVICE -> "Fatal (device)"
ERROR_CAMERA_DISABLED -> "Device policy"
ERROR_CAMERA_IN_USE -> "Camera in use"
ERROR_CAMERA_SERVICE -> "Fatal (service)"
ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
else -> "Unknown"
}
val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
Log.e(TAG, exc.message, exc)

cameraStateListener.onError(device, error)
if (cont.isActive) cont.resumeWithException(exc)
}
}, handler)
}

/**
* Creates a [CameraCaptureSession] and returns the configured session (as the result of the
* suspend coroutine)
*/
private suspend fun createCaptureSession(
device: CameraDevice,
targets: List<Surface>,
handler: Handler? = null
): CameraCaptureSession = suspendCoroutine { cont ->
// Creates a capture session using the predefined targets, and defines a session state
// callback which resumes the coroutine once the session is configured
device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {

override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)

override fun onConfigureFailed(session: CameraCaptureSession) {
val exc = RuntimeException("Camera ${device.id} session configuration failed")
Log.e(TAG, exc.message, exc)
cont.resumeWithException(exc)
}
}, handler)
}

fun stop() {
if (recordingStatus == STATE_RECORDING) {
mediaRecorderWrapper.forceStopRecording()
recordingStatus = STATE_IDLE
}
try {
camera.close()
} catch (exc: Throwable) {
Log.e(TAG, "Error closing camera", exc)
}
}

fun destroy() {
imageReaderPreview?.close()
cameraThread.quitSafely()
mediaRecorderWrapper.destroyRecording()
recorderSurface.release()
yubToBitmapConverter.destroy()
}

companion object {
private val TAG = CameraProvider::class.java.simpleName

const val CHOOSE_FILE_SAVE_DST = false

private const val STATE_IDLE = 0
private const val STATE_RECORDING = 1
private const val STATE_STARTING = 1
private const val STATE_STOPPING = 2

private val imageRetrieveSize = Size(640, 480)
}
}
Loading