From 9089014ed81ce906f39c343dcdf25af3cc2bce75 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 30 Jan 2024 15:28:18 +0100 Subject: [PATCH] feat: Add FPS Counter to Android (`enableFpsGraph`) (#2460) * feat: Add FPS Counter to Android (`enableFpsGraph`) * feat: Add FPS View * Update FpsCounterView.kt * Implement actual graph * fix layout * Update FpsGraphView.kt * Update CameraPage.tsx --- .../java/com/mrousavy/camera/CameraView.kt | 24 +++- .../com/mrousavy/camera/CameraViewManager.kt | 5 + .../java/com/mrousavy/camera/FpsGraphView.kt | 116 ++++++++++++++++++ 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/FpsGraphView.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index ff2b514..a3d86ff 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -81,22 +81,28 @@ class CameraView(context: Context) : previewView.resizeMode = value field = value } + var enableFpsGraph: Boolean = false + set(value) { + field = value + updateFpsGraph() + } // code scanner var codeScannerOptions: CodeScannerOptions? = null // private properties private var isMounted = false + private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher) internal val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager // session internal val cameraSession: CameraSession private val previewView: PreviewView private var currentConfigureCall: Long = System.currentTimeMillis() - internal var frameProcessor: FrameProcessor? = null - private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher) + // other + private var fpsGraph: FpsGraphView? = null init { this.installHierarchyFitter() @@ -225,8 +231,22 @@ class CameraView(context: Context) : } } + private fun updateFpsGraph() { + if (enableFpsGraph) { + if (fpsGraph != null) return + fpsGraph = FpsGraphView(context) + addView(fpsGraph) + } else { + if (fpsGraph == null) return + removeView(fpsGraph) + fpsGraph = null + } + } + override fun onFrame(frame: Frame) { frameProcessor?.call(frame) + + fpsGraph?.onTick() } override fun onError(error: Throwable) { diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt index 9c990d4..54f7fce 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt @@ -79,6 +79,11 @@ class CameraViewManager : ViewGroupManager() { view.enableZoomGesture = enableZoomGesture } + @ReactProp(name = "enableFpsGraph") + fun setEnableFpsGraph(view: CameraView, enableFpsGraph: Boolean) { + view.enableFpsGraph = enableFpsGraph + } + @ReactProp(name = "videoStabilizationMode") fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) { val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode) diff --git a/package/android/src/main/java/com/mrousavy/camera/FpsGraphView.kt b/package/android/src/main/java/com/mrousavy/camera/FpsGraphView.kt new file mode 100644 index 0000000..2db42a0 --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/FpsGraphView.kt @@ -0,0 +1,116 @@ +package com.mrousavy.camera + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import kotlin.math.max +import kotlin.math.roundToInt + +@SuppressLint("SetTextI18n") +class FpsGraphView(context: Context) : FrameLayout(context) { + private val textView = TextView(context) + private val graph = Graph(context) + + init { + textView.textSize = 18f + textView.setTextColor(Color.WHITE) + textView.text = "0 FPS" + + graph.callback = object : Graph.Callback { + override fun onAverageFpsChanged(averageFps: Int) { + textView.text = "$averageFps FPS" + } + } + + val layoutParams = LayoutParams(300, 150) + layoutParams.setMargins(15, 150, 0, 0) + layoutParams.gravity = Gravity.TOP or Gravity.LEFT + addView(graph, layoutParams) + addView(textView, layoutParams) + } + + fun onTick() { + graph.onTick() + } + + class Graph(context: Context) : + View(context), + Runnable { + private val maxBars = 30 + private val ticks = arrayListOf() + private val bars = arrayListOf() + private val paint = Paint().apply { + color = Color.RED + strokeWidth = 5f + style = Paint.Style.FILL + } + var callback: Callback? = null + + init { + post(this) + } + + override fun run() { + val averageFps = getAverageFps(ticks) + ticks.clear() + + bars.add(averageFps) + if (bars.size > maxBars) { + bars.removeAt(0) + } + + invalidate() + postDelayed(this, 1000) + + callback?.onAverageFpsChanged(averageFps) + } + + private fun getAverageFps(ticks: List): Int { + if (ticks.isEmpty()) return 0 + if (ticks.size < 2) return 1 + + val totalDuration = ticks.last() - ticks.first() + val averageFrameDuration = totalDuration / (ticks.size - 1).toDouble() + + val double = 1000.0 / averageFrameDuration + return double.roundToInt() + } + + fun onTick() { + val currentTick = System.currentTimeMillis() + ticks.add(currentTick.toDouble()) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (bars.size < 2) return + + val maxFps = max(bars.max().toFloat(), 60f) + val blockWidth = width.toFloat() / maxBars + for (i in 0..maxBars) { + val fps = bars.getOrNull(i) + if (fps != null) { + val blockHeight = (fps / maxFps) * height + canvas.drawRect( + i * blockWidth, + height - blockHeight, + (i + 1) * blockWidth, + height.toFloat(), + paint + ) + } + } + } + + interface Callback { + fun onAverageFpsChanged(averageFps: Int) + } + } +}