Skip to content

Commit f3c9a8d

Browse files
sbuggaymeta-codesync[bot]
authored andcommitted
Frame screenshots capture implementation (#54745)
Summary: Pull Request resolved: #54745 Generates and emits screenshots if enabled by DevTools Changelog: [Internal] Reviewed By: hoxyq Differential Revision: D88084756 fbshipit-source-id: 54f8a41f46836360952bee098ad6dda6c41ee537
1 parent 5bb3a6d commit f3c9a8d

2 files changed

Lines changed: 91 additions & 13 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/inspector/FrameTimingsObserver.kt

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,32 @@
77

88
package com.facebook.react.devsupport.inspector
99

10+
import android.graphics.Bitmap
1011
import android.os.Build
1112
import android.os.Handler
1213
import android.os.Looper
1314
import android.os.Process
15+
import android.util.Base64
1416
import android.view.FrameMetrics
17+
import android.view.PixelCopy
1518
import android.view.Window
1619
import com.facebook.proguard.annotations.DoNotStripAny
20+
import java.io.ByteArrayOutputStream
21+
import kotlin.coroutines.resume
22+
import kotlin.coroutines.suspendCoroutine
23+
import kotlinx.coroutines.CoroutineScope
24+
import kotlinx.coroutines.Dispatchers
25+
import kotlinx.coroutines.launch
1726

1827
@DoNotStripAny
1928
internal class FrameTimingsObserver(
2029
private val window: Window,
21-
onFrameTimingSequence: (sequence: FrameTimingSequence) -> Unit,
30+
private val screenshotsEnabled: Boolean,
31+
private val onFrameTimingSequence: (sequence: FrameTimingSequence) -> Unit,
2232
) {
2333
private val handler = Handler(Looper.getMainLooper())
2434
private var frameCounter: Int = 0
35+
private var bitmapBuffer: Bitmap? = null
2536

2637
private val frameMetricsListener =
2738
Window.OnFrameMetricsAvailableListener { _, frameMetrics, _dropCount ->
@@ -35,17 +46,77 @@ internal class FrameTimingsObserver(
3546
val endDrawingTimestamp =
3647
beginDrawingTimestamp + frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION)
3748

38-
onFrameTimingSequence(
39-
FrameTimingSequence(
40-
frameCounter++,
41-
Process.myTid(),
42-
beginDrawingTimestamp,
43-
commitTimestamp,
44-
endDrawingTimestamp,
45-
)
46-
)
49+
val frameId = frameCounter++
50+
val threadId = Process.myTid()
51+
52+
CoroutineScope(Dispatchers.Default).launch {
53+
val screenshot = if (screenshotsEnabled) captureScreenshot() else null
54+
55+
onFrameTimingSequence(
56+
FrameTimingSequence(
57+
frameId,
58+
threadId,
59+
beginDrawingTimestamp,
60+
commitTimestamp,
61+
endDrawingTimestamp,
62+
screenshot,
63+
)
64+
)
65+
}
4766
}
4867

68+
private suspend fun captureScreenshot(): String? = suspendCoroutine { continuation ->
69+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
70+
continuation.resume(null)
71+
return@suspendCoroutine
72+
}
73+
74+
val decorView = window.decorView
75+
val width = decorView.width
76+
val height = decorView.height
77+
78+
// Reuse bitmap if dimensions haven't changed
79+
val bitmap =
80+
bitmapBuffer?.let {
81+
if (it.width == width && it.height == height) {
82+
it
83+
} else {
84+
null
85+
}
86+
} ?: Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { bitmapBuffer = it }
87+
88+
PixelCopy.request(
89+
window,
90+
bitmap,
91+
{ copyResult ->
92+
if (copyResult == PixelCopy.SUCCESS) {
93+
CoroutineScope(Dispatchers.Default).launch {
94+
try {
95+
val scaleFactor = 0.5f
96+
val scaledWidth = (width * scaleFactor).toInt()
97+
val scaledHeight = (height * scaleFactor).toInt()
98+
val scaledBitmap =
99+
Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true)
100+
101+
val outputStream = ByteArrayOutputStream()
102+
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
103+
val jpegBytes = outputStream.toByteArray()
104+
val jpegBase64 = Base64.encodeToString(jpegBytes, Base64.NO_WRAP)
105+
continuation.resume(jpegBase64)
106+
107+
scaledBitmap.recycle()
108+
} catch (e: Exception) {
109+
continuation.resume(null)
110+
}
111+
}
112+
} else {
113+
continuation.resume(null)
114+
}
115+
},
116+
handler,
117+
)
118+
}
119+
49120
fun start() {
50121
frameCounter = 0
51122
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
@@ -62,5 +133,8 @@ internal class FrameTimingsObserver(
62133

63134
window.removeOnFrameMetricsAvailableListener(frameMetricsListener)
64135
handler.removeCallbacksAndMessages(null)
136+
137+
bitmapBuffer?.recycle()
138+
bitmapBuffer = null
65139
}
66140
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactHostImpl.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,9 +1565,13 @@ public class ReactHostImpl(
15651565
TracingState.ENABLED_IN_CDP_MODE -> {
15661566
currentActivity?.window?.let { window ->
15671567
val observer =
1568-
FrameTimingsObserver(window) { frameTimingsSequence ->
1569-
inspectorTarget.recordFrameTimings(frameTimingsSequence)
1570-
}
1568+
FrameTimingsObserver(
1569+
window,
1570+
_screenshotsEnabled,
1571+
{ frameTimingsSequence ->
1572+
inspectorTarget.recordFrameTimings(frameTimingsSequence)
1573+
},
1574+
)
15711575
observer.start()
15721576
frameTimingsObserver = observer
15731577
}

0 commit comments

Comments
 (0)