Skip to content

Commit ee8456d

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Add Page.captureScreenshot CDP support
Summary: 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 2bcb3e1 commit ee8456d

File tree

41 files changed

+475
-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

+475
-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<<c47da5069c3dfd7a287125bd0dfbc0c0>>
7+
* @generated SignedSource<<c99be3b2d8c18d23c2f1ae01d1c2dbae>>
88
*/
99

1010
/**
@@ -414,6 +414,12 @@ public object ReactNativeFeatureFlags {
414414
@JvmStatic
415415
public fun fuseboxNetworkInspectionEnabled(): Boolean = accessor.fuseboxNetworkInspectionEnabled()
416416

417+
/**
418+
* 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.
419+
*/
420+
@JvmStatic
421+
public fun fuseboxScreenshotCaptureEnabled(): Boolean = accessor.fuseboxScreenshotCaptureEnabled()
422+
417423
/**
418424
* Hides offscreen VirtualViews on iOS by setting hidden = YES to avoid extra cost of views
419425
*/

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<<0d98c7f7d2762c248a9670a447b5a93e>>
7+
* @generated SignedSource<<47bc6d114a86d061cc90653ab6baa7ba>>
88
*/
99

1010
/**
@@ -84,6 +84,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
8484
private var fuseboxEnabledReleaseCache: Boolean? = null
8585
private var fuseboxFrameRecordingEnabledCache: Boolean? = null
8686
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
87+
private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null
8788
private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null
8889
private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null
8990
private var perfIssuesEnabledCache: Boolean? = null
@@ -687,6 +688,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
687688
return cached
688689
}
689690

691+
override fun fuseboxScreenshotCaptureEnabled(): Boolean {
692+
var cached = fuseboxScreenshotCaptureEnabledCache
693+
if (cached == null) {
694+
cached = ReactNativeFeatureFlagsCxxInterop.fuseboxScreenshotCaptureEnabled()
695+
fuseboxScreenshotCaptureEnabledCache = cached
696+
}
697+
return cached
698+
}
699+
690700
override fun hideOffscreenVirtualViewsOnIOS(): Boolean {
691701
var cached = hideOffscreenVirtualViewsOnIOSCache
692702
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<<01dbfbb85643951c562e2cf1725387ff>>
7+
* @generated SignedSource<<299700cff60268da3af75e28572af537>>
88
*/
99

1010
/**
@@ -156,6 +156,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
156156

157157
@DoNotStrip @JvmStatic public external fun fuseboxNetworkInspectionEnabled(): Boolean
158158

159+
@DoNotStrip @JvmStatic public external fun fuseboxScreenshotCaptureEnabled(): Boolean
160+
159161
@DoNotStrip @JvmStatic public external fun hideOffscreenVirtualViewsOnIOS(): Boolean
160162

161163
@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<<79077f7e8b2a7d435ac44c0a31ab87cc>>
7+
* @generated SignedSource<<09f2e372547b6a23dc1341c899f16174>>
88
*/
99

1010
/**
@@ -151,6 +151,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
151151

152152
override fun fuseboxNetworkInspectionEnabled(): Boolean = true
153153

154+
override fun fuseboxScreenshotCaptureEnabled(): Boolean = false
155+
154156
override fun hideOffscreenVirtualViewsOnIOS(): Boolean = false
155157

156158
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<<b6c95e5954a185b0ee8c0ae8dde4a74b>>
7+
* @generated SignedSource<<f6aa644c758422884c9c82378680a309>>
88
*/
99

1010
/**
@@ -88,6 +88,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
8888
private var fuseboxEnabledReleaseCache: Boolean? = null
8989
private var fuseboxFrameRecordingEnabledCache: Boolean? = null
9090
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
91+
private var fuseboxScreenshotCaptureEnabledCache: Boolean? = null
9192
private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null
9293
private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null
9394
private var perfIssuesEnabledCache: Boolean? = null
@@ -755,6 +756,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
755756
return cached
756757
}
757758

759+
override fun fuseboxScreenshotCaptureEnabled(): Boolean {
760+
var cached = fuseboxScreenshotCaptureEnabledCache
761+
if (cached == null) {
762+
cached = currentProvider.fuseboxScreenshotCaptureEnabled()
763+
accessedFeatureFlags.add("fuseboxScreenshotCaptureEnabled")
764+
fuseboxScreenshotCaptureEnabledCache = cached
765+
}
766+
return cached
767+
}
768+
758769
override fun hideOffscreenVirtualViewsOnIOS(): Boolean {
759770
var cached = hideOffscreenVirtualViewsOnIOSCache
760771
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<<30a507b84b656705ee1abec794472d6b>>
7+
* @generated SignedSource<<3f5903c8157716a317ec95d2c497a1f3>>
88
*/
99

1010
/**
@@ -151,6 +151,8 @@ public interface ReactNativeFeatureFlagsProvider {
151151

152152
@DoNotStrip public fun fuseboxNetworkInspectionEnabled(): Boolean
153153

154+
@DoNotStrip public fun fuseboxScreenshotCaptureEnabled(): Boolean
155+
154156
@DoNotStrip public fun hideOffscreenVirtualViewsOnIOS(): Boolean
155157

156158
@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)