Skip to content

Commit 9ac5354

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Add Page.captureScreenshot CDP support (#56307)
Summary: Pull Request resolved: #56307 Implement the `Page.captureScreenshot` CDP command in the inspector backend, allowing DevTools to capture an on-demand screenshot of the current app view. This is a minimal implementation supporting only the `format` (jpeg/png/webp) and `quality` (0-100, jpeg only) parameters, returning base64-encoded image data. The feature is gated behind a new `fuseboxCaptureScreenshotEnabled` React Native feature flag, wired through `InspectorFlags` following the same pattern as frame recording. **Motivation** Improve agent verification / user feedback during AI sessions. We've proven that screenshots (in performance traces) are useful for understanding how components render on screen, and exposing this as an on-demand CDP method is relatively cheap today vs the larger task of modelling the DOM (Elements panel). **Changes** - New `fuseboxCaptureScreenshotEnabled` feature flag + `InspectorFlags` wiring (C++, Android JNI, Kotlin) - `HostTargetDelegate::captureScreenshot()` virtual method with async callback - C++ CDP handler in `HostAgent` — parses `format`/`quality`, delegates to platform, sends async CDP response (gated by flag) - iOS (`RCTHost.mm`): Captures key window via `UIGraphicsImageRenderer` + `drawViewHierarchyInRect`, encodes to PNG/JPEG - Android (`ReactHostImpl.kt`): Captures decor view via `Bitmap` + `Canvas` + `View.draw()`, encodes to PNG/JPEG/WebP - 4 C++ tests: success, failure, param forwarding, flag-disabled rejection Changelog: [Internal] Differential Revision: D99099930
1 parent 5eb1ca1 commit 9ac5354

File tree

41 files changed

+441
-48
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+441
-48
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ internal object InspectorFlags {
1919
SoLoader.loadLibrary("react_devsupportjni")
2020
}
2121

22+
@DoNotStrip @JvmStatic external fun getScreenshotCaptureEnabled(): Boolean
23+
2224
@DoNotStrip @JvmStatic external fun getFuseboxEnabled(): Boolean
2325

2426
@DoNotStrip @JvmStatic external fun getIsProfilingBuild(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<84bfc7b7a1b239c514b6d7c38dd91283>>
7+
* @generated SignedSource<<eacf6769e8598f88fe1790149e437f3f>>
88
*/
99

1010
/**
@@ -408,6 +408,12 @@ public object ReactNativeFeatureFlags {
408408
@JvmStatic
409409
public fun fuseboxNetworkInspectionEnabled(): Boolean = accessor.fuseboxNetworkInspectionEnabled()
410410

411+
/**
412+
* Enable Page.captureScreenshot CDP method support in the React Native DevTools CDP backend. This flag is global and should not be changed across React Host lifetimes.
413+
*/
414+
@JvmStatic
415+
public fun fuseboxScreenshotCaptureEnabled(): Boolean = accessor.fuseboxScreenshotCaptureEnabled()
416+
411417
/**
412418
* Hides offscreen VirtualViews on iOS by setting hidden = YES to avoid extra cost of views
413419
*/

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<1b59188082b9222b22b5cb0585cd166f>>
7+
* @generated SignedSource<<668885665a149d50bdff92bd96a297f0>>
88
*/
99

1010
/**
@@ -83,6 +83,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
8383
private var fuseboxEnabledReleaseCache: Boolean? = null
8484
private var fuseboxFrameRecordingEnabledCache: Boolean? = null
8585
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
86+
private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null
8687
private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null
8788
private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null
8889
private var perfIssuesEnabledCache: Boolean? = null
@@ -677,6 +678,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
677678
return cached
678679
}
679680

681+
override fun fuseboxScreenshotCaptureEnabled(): Boolean {
682+
var cached = fuseboxScreenshotCaptureEnabledCache
683+
if (cached == null) {
684+
cached = ReactNativeFeatureFlagsCxxInterop.fuseboxScreenshotCaptureEnabled()
685+
fuseboxScreenshotCaptureEnabledCache = cached
686+
}
687+
return cached
688+
}
689+
680690
override fun hideOffscreenVirtualViewsOnIOS(): Boolean {
681691
var cached = hideOffscreenVirtualViewsOnIOSCache
682692
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<2fd657c62d07ed766a9241cb1c14d98d>>
7+
* @generated SignedSource<<aa4a2ab7af66d857da4318ac2e75899b>>
88
*/
99

1010
/**
@@ -154,6 +154,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
154154

155155
@DoNotStrip @JvmStatic public external fun fuseboxNetworkInspectionEnabled(): Boolean
156156

157+
@DoNotStrip @JvmStatic public external fun fuseboxScreenshotCaptureEnabled(): Boolean
158+
157159
@DoNotStrip @JvmStatic public external fun hideOffscreenVirtualViewsOnIOS(): Boolean
158160

159161
@DoNotStrip @JvmStatic public external fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<f3204842cd731d7eff8c4c4eeeead515>>
7+
* @generated SignedSource<<6348c0cc9285f2bac6df9986155b584f>>
88
*/
99

1010
/**
@@ -149,6 +149,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
149149

150150
override fun fuseboxNetworkInspectionEnabled(): Boolean = true
151151

152+
override fun fuseboxScreenshotCaptureEnabled(): Boolean = false
153+
152154
override fun hideOffscreenVirtualViewsOnIOS(): Boolean = false
153155

154156
override fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean = false

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<251381892c7d15310d61b35913c5cba6>>
7+
* @generated SignedSource<<f545945b19339923fcafab4eeb1da134>>
88
*/
99

1010
/**
@@ -87,6 +87,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
8787
private var fuseboxEnabledReleaseCache: Boolean? = null
8888
private var fuseboxFrameRecordingEnabledCache: Boolean? = null
8989
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
90+
private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null
9091
private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null
9192
private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null
9293
private var perfIssuesEnabledCache: Boolean? = null
@@ -744,6 +745,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
744745
return cached
745746
}
746747

748+
override fun fuseboxScreenshotCaptureEnabled(): Boolean {
749+
var cached = fuseboxScreenshotCaptureEnabledCache
750+
if (cached == null) {
751+
cached = currentProvider.fuseboxScreenshotCaptureEnabled()
752+
accessedFeatureFlags.add("fuseboxScreenshotCaptureEnabled")
753+
fuseboxScreenshotCaptureEnabledCache = cached
754+
}
755+
return cached
756+
}
757+
747758
override fun hideOffscreenVirtualViewsOnIOS(): Boolean {
748759
var cached = hideOffscreenVirtualViewsOnIOSCache
749760
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<7e47ac680222281a813e65484e7f8e39>>
7+
* @generated SignedSource<<e340fe9805381dcf818f51e333f7c120>>
88
*/
99

1010
/**
@@ -149,6 +149,8 @@ public interface ReactNativeFeatureFlagsProvider {
149149

150150
@DoNotStrip public fun fuseboxNetworkInspectionEnabled(): Boolean
151151

152+
@DoNotStrip public fun fuseboxScreenshotCaptureEnabled(): Boolean
153+
152154
@DoNotStrip public fun hideOffscreenVirtualViewsOnIOS(): Boolean
153155

154156
@DoNotStrip public fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import android.content.Context
1212
import android.content.Intent
1313
import android.nfc.NfcAdapter
1414
import android.os.Bundle
15+
import androidx.core.graphics.createBitmap
1516
import com.facebook.common.logging.FLog
1617
import com.facebook.infer.annotation.Assertions
1718
import com.facebook.infer.annotation.ThreadConfined
@@ -447,6 +448,43 @@ public class ReactHostImpl(
447448
InspectorNetworkHelper.loadNetworkResource(url, listener)
448449
}
449450

451+
@DoNotStrip
452+
private fun captureScreenshot(format: String, quality: Int): String? {
453+
val activity = currentActivity ?: return null
454+
val window = activity.window ?: return null
455+
val decorView = window.decorView.rootView
456+
457+
val width = decorView.width
458+
val height = decorView.height
459+
if (width <= 0 || height <= 0) {
460+
return null
461+
}
462+
463+
val bitmap = createBitmap(width, height)
464+
val canvas = android.graphics.Canvas(bitmap)
465+
decorView.draw(canvas)
466+
467+
val outputStream = java.io.ByteArrayOutputStream()
468+
val compressFormat =
469+
when (format) {
470+
"jpeg" -> android.graphics.Bitmap.CompressFormat.JPEG
471+
"webp" ->
472+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
473+
android.graphics.Bitmap.CompressFormat.WEBP_LOSSY
474+
} else {
475+
@Suppress("DEPRECATION") android.graphics.Bitmap.CompressFormat.WEBP
476+
}
477+
else -> android.graphics.Bitmap.CompressFormat.PNG
478+
}
479+
val compressQuality = if (quality in 0..100) quality else 80
480+
481+
bitmap.compress(compressFormat, compressQuality, outputStream)
482+
bitmap.recycle()
483+
484+
val bytes = outputStream.toByteArray()
485+
return android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP)
486+
}
487+
450488
/**
451489
* Entrypoint to destroy the ReactInstance. If the ReactInstance is reloading, will wait until
452490
* reload is finished, before destroying.

packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111

1212
namespace facebook::react::jsinspector_modern {
1313

14+
bool JInspectorFlags::getScreenshotCaptureEnabled(
15+
jni::alias_ref<jclass> /*unused*/) {
16+
auto& inspectorFlags = InspectorFlags::getInstance();
17+
return inspectorFlags.getScreenshotCaptureEnabled();
18+
}
19+
1420
bool JInspectorFlags::getFuseboxEnabled(jni::alias_ref<jclass> /*unused*/) {
1521
auto& inspectorFlags = InspectorFlags::getInstance();
1622
return inspectorFlags.getFuseboxEnabled();
@@ -28,6 +34,11 @@ bool JInspectorFlags::getFrameRecordingEnabled(
2834
}
2935

3036
void JInspectorFlags::registerNatives() {
37+
javaClassLocal()->registerNatives({
38+
makeNativeMethod(
39+
"getScreenshotCaptureEnabled",
40+
JInspectorFlags::getScreenshotCaptureEnabled),
41+
});
3142
javaClassLocal()->registerNatives({
3243
makeNativeMethod("getFuseboxEnabled", JInspectorFlags::getFuseboxEnabled),
3344
});

packages/react-native/ReactAndroid/src/main/jni/react/devsupport/JInspectorFlags.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class JInspectorFlags : public jni::JavaClass<JInspectorFlags> {
1818
public:
1919
static constexpr auto kJavaDescriptor = "Lcom/facebook/react/devsupport/InspectorFlags;";
2020

21+
static bool getScreenshotCaptureEnabled(jni::alias_ref<jclass> /*unused*/);
2122
static bool getFuseboxEnabled(jni::alias_ref<jclass> /*unused*/);
2223
static bool getIsProfilingBuild(jni::alias_ref<jclass> /*unused*/);
2324
static bool getFrameRecordingEnabled(jni::alias_ref<jclass> /*unused*/);

0 commit comments

Comments
 (0)