Commit d202a1ff authored by Gaëtan Renaudeau's avatar Gaëtan Renaudeau

Implement options for captureFrame

- format: "base64" || "file"
- type: "png" || "jpeg"/"jpg" || "webm" (android only)
- quality: from 0 to 1 (iOS support only with type=jpg/jpeg)
- filePath: for format=="file" you must provide a file path (you can use react-native-fs to get directory constants)
parent 8acaae75
...@@ -112,6 +112,7 @@ ...@@ -112,6 +112,7 @@
<orderEntry type="library" exported="" name="drawee-0.8.1" level="project" /> <orderEntry type="library" exported="" name="drawee-0.8.1" level="project" />
<orderEntry type="library" exported="" name="appcompat-v7-23.0.1" level="project" /> <orderEntry type="library" exported="" name="appcompat-v7-23.0.1" level="project" />
<orderEntry type="library" exported="" name="support-annotations-23.0.1" level="project" /> <orderEntry type="library" exported="" name="support-annotations-23.0.1" level="project" />
<orderEntry type="module" module-name="react-native-fs" exported="" />
<orderEntry type="module" module-name="RNMaterialKit" exported="" /> <orderEntry type="module" module-name="RNMaterialKit" exported="" />
<orderEntry type="module" module-name="RNGL" exported="" /> <orderEntry type="module" module-name="RNGL" exported="" />
</component> </component>
......
...@@ -78,4 +78,6 @@ dependencies { ...@@ -78,4 +78,6 @@ dependencies {
compile project(":RNMaterialKit") compile project(":RNMaterialKit")
compile project(":RNGL") compile project(":RNGL")
compile project(':react-native-fs')
} }
...@@ -11,6 +11,7 @@ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; ...@@ -11,6 +11,7 @@ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.shell.MainReactPackage; import com.facebook.react.shell.MainReactPackage;
import com.github.xinthink.rnmk.ReactMaterialKitPackage; import com.github.xinthink.rnmk.ReactMaterialKitPackage;
import com.projectseptember.RNGL.RNGLPackage; import com.projectseptember.RNGL.RNGLPackage;
import com.rnfs.RNFSPackage;
public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler { public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler {
...@@ -28,6 +29,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand ...@@ -28,6 +29,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
.setJSMainModuleName("index.android") .setJSMainModuleName("index.android")
.addPackage(new MainReactPackage()) .addPackage(new MainReactPackage())
.addPackage(new RNGLPackage()) .addPackage(new RNGLPackage())
.addPackage(new RNFSPackage())
.addPackage(new ReactMaterialKitPackage()) .addPackage(new ReactMaterialKitPackage())
.setUseDeveloperSupport(BuildConfig.DEBUG) .setUseDeveloperSupport(BuildConfig.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED) .setInitialLifecycleState(LifecycleState.RESUMED)
......
...@@ -7,3 +7,6 @@ project(':RNMaterialKit').projectDir = file('../node_modules/react-native-materi ...@@ -7,3 +7,6 @@ project(':RNMaterialKit').projectDir = file('../node_modules/react-native-materi
include ':RNGL' include ':RNGL'
project(':RNGL').projectDir = file('../../../android') project(':RNGL').projectDir = file('../../../android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(settingsDir, '../node_modules/react-native-fs/android')
\ No newline at end of file
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
34607B601C132B22009203B1 /* libRCTMaterialKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 34607B581C132B1C009203B1 /* libRCTMaterialKit.a */; }; 34607B601C132B22009203B1 /* libRCTMaterialKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 34607B581C132B1C009203B1 /* libRCTMaterialKit.a */; };
3461EB4B1C132AEC0003E4A2 /* libRNGL.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3461EB4A1C132AD30003E4A2 /* libRNGL.a */; }; 3461EB4B1C132AEC0003E4A2 /* libRNGL.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3461EB4A1C132AD30003E4A2 /* libRNGL.a */; };
34C9905A1C3530D6002F49FC /* libRNFS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 34C990581C3530CE002F49FC /* libRNFS.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
...@@ -104,6 +105,13 @@ ...@@ -104,6 +105,13 @@
remoteGlobalIDString = 4107012F1ACB723B00C6AA39; remoteGlobalIDString = 4107012F1ACB723B00C6AA39;
remoteInfo = RNGL; remoteInfo = RNGL;
}; };
34C990571C3530CE002F49FC /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 34C990481C3530CE002F49FC /* RNFS.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = F12AFB9B1ADAF8F800E0535D;
remoteInfo = RNFS;
};
78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */;
...@@ -142,6 +150,7 @@ ...@@ -142,6 +150,7 @@
146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = "<group>"; }; 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = "<group>"; };
34607B4F1C132B1C009203B1 /* RCTMaterialKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTMaterialKit.xcodeproj; path = "../node_modules/react-native-material-kit/iOS/RCTMaterialKit.xcodeproj"; sourceTree = "<group>"; }; 34607B4F1C132B1C009203B1 /* RCTMaterialKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTMaterialKit.xcodeproj; path = "../node_modules/react-native-material-kit/iOS/RCTMaterialKit.xcodeproj"; sourceTree = "<group>"; };
3461EB3B1C132AD30003E4A2 /* RNGL.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNGL.xcodeproj; path = "../node_modules/gl-react-native/ios/RNGL.xcodeproj"; sourceTree = "<group>"; }; 3461EB3B1C132AD30003E4A2 /* RNGL.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNGL.xcodeproj; path = "../node_modules/gl-react-native/ios/RNGL.xcodeproj"; sourceTree = "<group>"; };
34C990481C3530CE002F49FC /* RNFS.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNFS.xcodeproj; path = "../node_modules/react-native-fs/RNFS.xcodeproj"; sourceTree = "<group>"; };
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
...@@ -158,6 +167,7 @@ ...@@ -158,6 +167,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
34C9905A1C3530D6002F49FC /* libRNFS.a in Frameworks */,
34607B601C132B22009203B1 /* libRCTMaterialKit.a in Frameworks */, 34607B601C132B22009203B1 /* libRCTMaterialKit.a in Frameworks */,
3461EB4B1C132AEC0003E4A2 /* libRNGL.a in Frameworks */, 3461EB4B1C132AEC0003E4A2 /* libRNGL.a in Frameworks */,
146834051AC3E58100842450 /* libReact.a in Frameworks */, 146834051AC3E58100842450 /* libReact.a in Frameworks */,
...@@ -287,6 +297,14 @@ ...@@ -287,6 +297,14 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
34C990491C3530CE002F49FC /* Products */ = {
isa = PBXGroup;
children = (
34C990581C3530CE002F49FC /* libRNFS.a */,
);
name = Products;
sourceTree = "<group>";
};
78C398B11ACF4ADC00677621 /* Products */ = { 78C398B11ACF4ADC00677621 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
...@@ -298,6 +316,7 @@ ...@@ -298,6 +316,7 @@
832341AE1AAA6A7D00B99B32 /* Libraries */ = { 832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
34C990481C3530CE002F49FC /* RNFS.xcodeproj */,
34607B4F1C132B1C009203B1 /* RCTMaterialKit.xcodeproj */, 34607B4F1C132B1C009203B1 /* RCTMaterialKit.xcodeproj */,
3461EB3B1C132AD30003E4A2 /* RNGL.xcodeproj */, 3461EB3B1C132AD30003E4A2 /* RNGL.xcodeproj */,
146833FF1AC3E56700842450 /* React.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */,
...@@ -453,6 +472,10 @@ ...@@ -453,6 +472,10 @@
ProductGroup = 146834001AC3E56700842450 /* Products */; ProductGroup = 146834001AC3E56700842450 /* Products */;
ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */;
}, },
{
ProductGroup = 34C990491C3530CE002F49FC /* Products */;
ProjectRef = 34C990481C3530CE002F49FC /* RNFS.xcodeproj */;
},
{ {
ProductGroup = 3461EB3C1C132AD30003E4A2 /* Products */; ProductGroup = 3461EB3C1C132AD30003E4A2 /* Products */;
ProjectRef = 3461EB3B1C132AD30003E4A2 /* RNGL.xcodeproj */; ProjectRef = 3461EB3B1C132AD30003E4A2 /* RNGL.xcodeproj */;
...@@ -537,6 +560,13 @@ ...@@ -537,6 +560,13 @@
remoteRef = 3461EB491C132AD30003E4A2 /* PBXContainerItemProxy */; remoteRef = 3461EB491C132AD30003E4A2 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR; sourceTree = BUILT_PRODUCTS_DIR;
}; };
34C990581C3530CE002F49FC /* libRNFS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRNFS.a;
remoteRef = 34C990571C3530CE002F49FC /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = {
isa = PBXReferenceProxy; isa = PBXReferenceProxy;
fileType = archive.ar; fileType = archive.ar;
......
...@@ -6,9 +6,10 @@ ...@@ -6,9 +6,10 @@
"start": "react-native start" "start": "react-native start"
}, },
"dependencies": { "dependencies": {
"gl-react-native": "file:../..",
"gl-react": "^2.0.8", "gl-react": "^2.0.8",
"gl-react-native": "file:../..",
"react-native": "^0.17.0", "react-native": "^0.17.0",
"react-native-fs": "^1.1.0",
"react-native-material-kit": "^0.2.4" "react-native-material-kit": "^0.2.4"
} }
} }
...@@ -22,6 +22,8 @@ const { ...@@ -22,6 +22,8 @@ const {
MKButton, MKButton,
} = require("react-native-material-kit"); } = require("react-native-material-kit");
const RNFS = require("react-native-fs");
const HelloGL = require("./HelloGL"); const HelloGL = require("./HelloGL");
const Saturation = require("./Saturation"); const Saturation = require("./Saturation");
const HueRotate = require("./HueRotate"); const HueRotate = require("./HueRotate");
...@@ -82,7 +84,8 @@ class Simple extends Component { ...@@ -82,7 +84,8 @@ class Simple extends Component {
switch1: false, switch1: false,
switch2: false, switch2: false,
switch3: false, switch3: false,
captured: null captured: null,
captureConfig: null
}; };
this.onCapture1 = this.onCapture1.bind(this); this.onCapture1 = this.onCapture1.bind(this);
...@@ -97,9 +100,17 @@ class Simple extends Component { ...@@ -97,9 +100,17 @@ class Simple extends Component {
} }
onCapture1 () { onCapture1 () {
this.refs.helloGL.captureFrame().then(data64 => { const captureConfig = {
this.setState({ captured: data64 }); quality: Math.round((Math.random() * 100))/100,
}); type: Math.random() < 0.5 ? "jpg": "png",
format: Math.random() < 0.5 ? "base64" : "file"
};
if (captureConfig.format === "file") {
captureConfig.filePath = RNFS.DocumentDirectoryPath+"/hellogl_capture.png";
}
this.refs.helloGL
.captureFrame(captureConfig)
.then(captured => this.setState({ captured, captureConfig }));
} }
render () { render () {
...@@ -113,7 +124,8 @@ class Simple extends Component { ...@@ -113,7 +124,8 @@ class Simple extends Component {
switch1, switch1,
switch2, switch2,
switch3, switch3,
captured captured,
captureConfig
} = this.state; } = this.state;
return <View style={styles.container}> return <View style={styles.container}>
...@@ -128,9 +140,28 @@ class Simple extends Component { ...@@ -128,9 +140,28 @@ class Simple extends Component {
</Surface> </Surface>
<View style={{ paddingTop: 20, alignItems: "center", flexDirection: "row" }}> <View style={{ paddingTop: 20, alignItems: "center", flexDirection: "row" }}>
<Button onPress={this.onCapture1}>captureFrame()</Button> <Button onPress={this.onCapture1}>captureFrame()</Button>
{captured && <Image source={{ uri: captured }} style={{ marginLeft: 20, width: 51, height: 34 }} />} {captured &&
<Image source={{ uri: captured }}
style={{ marginLeft: 20, width: 51, height: 34 }}
/> }
</View>
{captureConfig &&
<View style={{ paddingTop: 20, alignItems: "center", flexDirection: "row", justifyContent: "space-between" }}>
<Text style={{ fontSize: 10 }}>
format={captureConfig.format}
</Text>
<Text style={{ fontSize: 10 }}>
type={captureConfig.type}
</Text>
<Text style={{ fontSize: 10 }}>
quality={captureConfig.quality+""}
</Text>
</View> </View>
{captured && <Text style={{ marginTop: 10, fontSize: 10, fontFamily: "Cochin" }} numberOfLines={1}>{captured.slice(0, 100)}</Text>} }
{captured &&
<Text numberOfLines={1} style={{ marginTop: 10, fontSize: 10, color: "#aaa" }}>
{captured.slice(0, 100)}
</Text> }
</Demo> </Demo>
<Demo id={2} title="2. Saturate an Image"> <Demo id={2} title="2. Saturate an Image">
......
package com.projectseptember.RNGL;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
public class CaptureConfig {
String format;
String type;
String filePath;
Double quality;
public CaptureConfig(String format, String type, String filePath, Double quality) {
this.format = format;
this.type = type;
this.filePath = filePath;
this.quality = quality;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CaptureConfig that = (CaptureConfig) o;
if (format != null ? !format.equals(that.format) : that.format != null) return false;
if (type != null ? !type.equals(that.type) : that.type != null) return false;
if (filePath != null ? !filePath.equals(that.filePath) : that.filePath != null)
return false;
return !(quality != null ? !quality.equals(that.quality) : that.quality != null);
}
@Override
public int hashCode() {
int result = format != null ? format.hashCode() : 0;
result = 31 * result + (type != null ? type.hashCode() : 0);
result = 31 * result + (filePath != null ? filePath.hashCode() : 0);
result = 31 * result + (quality != null ? quality.hashCode() : 0);
return result;
}
public static CaptureConfig fromMap (ReadableMap map) {
return new CaptureConfig(
map.getString("format"),
map.getString("type"),
map.getString("filePath"),
map.getDouble("quality")
);
}
public WritableMap toMap () {
WritableMap map = Arguments.createMap();
map.putString("format", format);
map.putString("type", type);
map.putString("filePath", filePath);
map.putDouble("quality", quality);
return map;
}
}
...@@ -27,6 +27,7 @@ import com.facebook.react.uimanager.ThemedReactContext; ...@@ -27,6 +27,7 @@ import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.events.RCTEventEmitter; import com.facebook.react.uimanager.events.RCTEventEmitter;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
...@@ -69,7 +70,8 @@ public class GLCanvas extends GLSurfaceView ...@@ -69,7 +70,8 @@ public class GLCanvas extends GLSurfaceView
private Map<Integer, GLFBO> fbos; private Map<Integer, GLFBO> fbos;
private ExecutorSupplier executorSupplier; private ExecutorSupplier executorSupplier;
private final Queue<Runnable> mRunOnDraw = new LinkedList<>(); private final Queue<Runnable> mRunOnDraw = new LinkedList<>();
private boolean captureFrameRequested = false;
private List<CaptureConfig> captureConfigs = new ArrayList<>();
private float pixelRatio; private float pixelRatio;
private float displayDensity; private float displayDensity;
...@@ -169,17 +171,75 @@ public class GLCanvas extends GLSurfaceView ...@@ -169,17 +171,75 @@ public class GLCanvas extends GLSurfaceView
if (shouldRenderNow) { if (shouldRenderNow) {
this.render(); this.render();
deferredRendering = false; deferredRendering = false;
if (captureFrameRequested) { if (captureConfigs.size() > 0) {
captureFrameRequested = false; capture(); // FIXME: maybe we should schedule this?
}
}
}
private void capture () {
Bitmap capture = createSnapshot(); Bitmap capture = createSnapshot();
ReactContext reactContext = (ReactContext)getContext();
RCTEventEmitter eventEmitter = reactContext.getJSModule(RCTEventEmitter.class);
for (CaptureConfig config : captureConfigs) {
String result = null, error = null;
boolean isPng = config.type.equals("png");
boolean isJpeg = !isPng && (config.type.equals("jpg")||config.type.equals("jpeg"));
boolean isWebm = !isPng && !isJpeg && config.type.equals("webm");
boolean isBase64 = config.format.equals("base64");
boolean isFile = !isBase64 && config.format.equals("file");
Bitmap.CompressFormat compressFormat =
isPng ? Bitmap.CompressFormat.PNG :
isJpeg ? Bitmap.CompressFormat.JPEG :
isWebm ? Bitmap.CompressFormat.WEBP :
null;
int quality = (int)(100 * config.quality);
if (compressFormat == null) {
error = "Unsupported capture type '"+config.type+"'";
}
else if (isBase64) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
capture.compress(Bitmap.CompressFormat.PNG, 100, baos); capture.compress(compressFormat, quality, baos);
String frame = "data:image/png;base64,"+ String frame = "data:image/png;base64,"+
Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT); Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT);
dispatchOnCaptureFrame(frame); baos.close();
result = frame;
}
catch (Exception e) {
e.printStackTrace();
error = "Could not capture as base64: "+e.getMessage();
}
}
else if (isFile) {
try {
FileOutputStream fileOutputStream = new FileOutputStream(config.filePath);
capture.compress(compressFormat, quality, fileOutputStream);
fileOutputStream.close();
result = "file://"+config.filePath;
} }
catch (Exception e) {
e.printStackTrace();
error = "Could not write file: "+e.getMessage();
} }
} }
else {
error = "Unsupported capture format '"+config.format+"'";
}
WritableMap response = Arguments.createMap();
response.putMap("config", config.toMap());
if (error != null) response.putString("error", error);
if (result != null) response.putString("result", result);
eventEmitter.receiveEvent(getId(), "captureFrame", response);
}
captureConfigs = new ArrayList<>();
}
private boolean haveRemainingToPreload() { private boolean haveRemainingToPreload() {
for (Uri uri: imagesToPreload) { for (Uri uri: imagesToPreload) {
...@@ -677,16 +737,6 @@ public class GLCanvas extends GLSurfaceView ...@@ -677,16 +737,6 @@ public class GLCanvas extends GLSurfaceView
} }
} }
private void dispatchOnCaptureFrame (String frame) {
WritableMap event = Arguments.createMap();
event.putString("frame", frame);
ReactContext reactContext = (ReactContext)getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
getId(),
"captureFrame",
event);
}
private void dispatchOnProgress (double progress, int loaded, int total) { private void dispatchOnProgress (double progress, int loaded, int total) {
WritableMap event = Arguments.createMap(); WritableMap event = Arguments.createMap();
event.putDouble("progress", Double.isNaN(progress) ? 0.0 : progress); event.putDouble("progress", Double.isNaN(progress) ? 0.0 : progress);
...@@ -708,9 +758,14 @@ public class GLCanvas extends GLSurfaceView ...@@ -708,9 +758,14 @@ public class GLCanvas extends GLSurfaceView
event); event);
} }
public void requestCaptureFrame() { public void requestCaptureFrame (CaptureConfig config) {
captureFrameRequested = true;
this.requestRender(); this.requestRender();
for (CaptureConfig existing : captureConfigs) {
if (existing.equals(config)) {
return;
}
}
captureConfigs.add(config);
} }
private Bitmap createSnapshot () { private Bitmap createSnapshot () {
......
...@@ -94,7 +94,7 @@ public class GLCanvasManager extends SimpleViewManager<GLCanvas> { ...@@ -94,7 +94,7 @@ public class GLCanvasManager extends SimpleViewManager<GLCanvas> {
Assertions.assertNotNull(args); Assertions.assertNotNull(args);
switch (commandType) { switch (commandType) {
case COMMAND_CAPTURE_FRAME: { case COMMAND_CAPTURE_FRAME: {
canvas.requestCaptureFrame(); canvas.requestCaptureFrame(CaptureConfig.fromMap(args.getMap(0)));
return; return;
} }
default: default:
......
#import <Foundation/Foundation.h>
@interface CaptureConfig: NSObject
@property (nonatomic, copy) NSString *format;
@property (nonatomic, copy) NSString *type;
@property (nonatomic, copy) NSString *filePath;
@property (nonatomic, copy) NSNumber *quality;
-(instancetype)initWithFormat: (NSString *)format
withType: (NSString *)type
withQuality: (NSNumber *)quality
withFilePath: (NSString *)filePath;
- (bool) isEqualToCaptureConfig: (CaptureConfig *)other;
- (NSDictionary *) dictionary;
@end
#import "CaptureConfig.h"
@implementation CaptureConfig
-(instancetype)initWithFormat: (NSString *)format
withType: (NSString *)type
withQuality: (NSNumber *)quality
withFilePath: (NSString *)filePath
{
if ((self = [super init])) {
self.format = format;
self.type = type;
self.quality = quality;
self.filePath = filePath;
}
return self;
}
- (bool) isEqualToCaptureConfig: (CaptureConfig *)other
{
return [self.format isEqualToString:other.format] &&
[self.type isEqualToString:other.type] &&
[self.quality isEqualToNumber:other.quality] &&
[self.filePath isEqualToString:other.filePath];
}
- (NSDictionary *) dictionary
{
return @{
@"format": self.format,
@"type": self.type,
@"quality": self.quality,
@"filePath": self.filePath
};
}
@end
#import <GLKit/GLKit.h> #import <GLKit/GLKit.h>
#import "GLData.h" #import "GLData.h"
#import "CaptureConfig.h"
#import "RCTComponent.h" #import "RCTComponent.h"
@interface GLCanvas: GLKView @interface GLCanvas: GLKView
...@@ -19,6 +20,6 @@ ...@@ -19,6 +20,6 @@
- (instancetype)initWithBridge:(RCTBridge *)bridge; - (instancetype)initWithBridge:(RCTBridge *)bridge;
- (void) requestCaptureFrame; - (void) requestCaptureFrame:(CaptureConfig *)config;
@end @end
...@@ -44,8 +44,6 @@ NSArray* diff (NSArray* a, NSArray* b) { ...@@ -44,8 +44,6 @@ NSArray* diff (NSArray* a, NSArray* b) {
GLRenderData *_renderData; GLRenderData *_renderData;
BOOL _captureFrameRequested;
NSArray *_contentData; NSArray *_contentData;
NSArray *_contentTextures; NSArray *_contentTextures;
NSDictionary *_images; // This caches the currently used images (imageSrc -> GLReactImage) NSDictionary *_images; // This caches the currently used images (imageSrc -> GLReactImage)
...@@ -63,6 +61,9 @@ NSArray* diff (NSArray* a, NSArray* b) { ...@@ -63,6 +61,9 @@ NSArray* diff (NSArray* a, NSArray* b) {
NSTimer *animationTimer; NSTimer *animationTimer;
BOOL _needSync; BOOL _needSync;
NSMutableArray *_captureConfigs;
BOOL _captureScheduled;
} }
- (instancetype)initWithBridge:(RCTBridge *)bridge - (instancetype)initWithBridge:(RCTBridge *)bridge
...@@ -71,7 +72,8 @@ NSArray* diff (NSArray* a, NSArray* b) { ...@@ -71,7 +72,8 @@ NSArray* diff (NSArray* a, NSArray* b) {
_bridge = bridge; _bridge = bridge;
_images = @{}; _images = @{};
_preloaded = [[NSMutableArray alloc] init]; _preloaded = [[NSMutableArray alloc] init];
_captureFrameRequested = false; _captureConfigs = [[NSMutableArray alloc] init];
_captureScheduled = false;
_dirtyOnLoad = true; _dirtyOnLoad = true;
_neverRendered = true; _neverRendered = true;
self.context = [bridge.rnglContext getContext]; self.context = [bridge.rnglContext getContext];
...@@ -83,10 +85,15 @@ RCT_NOT_IMPLEMENTED(-init) ...@@ -83,10 +85,15 @@ RCT_NOT_IMPLEMENTED(-init)
//// Props Setters //// Props Setters
- (void) requestCaptureFrame - (void) requestCaptureFrame: (CaptureConfig *)config
{ {
_captureFrameRequested = true;
[self setNeedsDisplay]; [self setNeedsDisplay];
for (CaptureConfig *existing in _captureConfigs) {
if ([existing isEqualToCaptureConfig:config]) {
return;
}
}
[_captureConfigs addObject:config];
} }
-(void)setImagesToPreload:(NSArray *)imagesToPreload -(void)setImagesToPreload:(NSArray *)imagesToPreload
...@@ -383,21 +390,63 @@ RCT_NOT_IMPLEMENTED(-init) ...@@ -383,21 +390,63 @@ RCT_NOT_IMPLEMENTED(-init)
if (willRender) { if (willRender) {
[self render]; [self render];
if (_captureFrameRequested) { if (!_captureScheduled && [_captureConfigs count] > 0) {
_captureFrameRequested = false; _captureScheduled = true;
[self performSelectorOnMainThread:@selector(capture) withObject:nil waitUntilDone:NO]; [self performSelectorOnMainThread:@selector(capture) withObject:nil waitUntilDone:NO];
} }
} }
} }
-(void)capture -(void) capture
{ {
_captureScheduled = false;
if (!self.onGLCaptureFrame) return;
UIImage *frameImage = [self snapshot]; UIImage *frameImage = [self snapshot];
NSData *frameData = UIImagePNGRepresentation(frameImage);
NSString *frame = for (CaptureConfig *config in _captureConfigs) {
[NSString stringWithFormat:@"data:image/png;base64,%@", id result;
[frameData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]]; id error;
if (self.onGLCaptureFrame) self.onGLCaptureFrame(@{ @"frame": frame });
BOOL isPng = [config.type isEqualToString:@"png"];
BOOL isJpeg = !isPng && ([config.type isEqualToString:@"jpeg"] || [config.type isEqualToString:@"jpg"]);
BOOL isBase64 = [config.format isEqualToString:@"base64"];
BOOL isFile = !isBase64 && [config.format isEqualToString:@"file"];
NSData *frameData =
isPng ? UIImagePNGRepresentation(frameImage) :
isJpeg ? UIImageJPEGRepresentation(frameImage, [config.quality floatValue]) :
nil;
if (!frameData) {
error = [NSString stringWithFormat:@"Unsupported capture type '%@'", config.type];
}
else if (isBase64) {
NSString *base64 = [frameData base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength];
result = [NSString stringWithFormat:@"data:image/%@;base64,%@", config.type, base64];
}
else if (isFile) {
NSError *e;
if (![frameData writeToFile:config.filePath options:0 error:&e]) {
error = [NSString stringWithFormat:@"Could not write file: %@", e.localizedDescription];
}
else {
result = [NSString stringWithFormat:@"file://%@", config.filePath];
}
}
else {
error = [NSString stringWithFormat:@"Unsupported capture format '%@'", config.format];
}
NSMutableDictionary *response = [[NSMutableDictionary alloc] init];
response[@"config"] = [config dictionary];
if (error) response[@"error"] = error;
if (result) response[@"result"] = result;
self.onGLCaptureFrame(response);
}
_captureConfigs = [[NSMutableArray alloc] init];
} }
- (void)render - (void)render
......
#import "GLCanvasManager.h" #import "GLCanvasManager.h"
#import "GLCanvas.h" #import "GLCanvas.h"
#import "RCTConvert+GLData.h" #import "RCTConvert+GLData.h"
#import "RCTConvert+CaptureConfig.h"
#import "RCTUIManager.h" #import "RCTUIManager.h"
#import "RCTLog.h" #import "RCTLog.h"
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
...@@ -28,7 +29,7 @@ RCT_EXPORT_VIEW_PROPERTY(onGLLoad, RCTBubblingEventBlock); ...@@ -28,7 +29,7 @@ RCT_EXPORT_VIEW_PROPERTY(onGLLoad, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onGLProgress, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onGLProgress, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onGLCaptureFrame, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onGLCaptureFrame, RCTBubblingEventBlock);
RCT_EXPORT_METHOD(capture: (nonnull NSNumber *)reactTag) RCT_EXPORT_METHOD(capture: (nonnull NSNumber *)reactTag withConfig:(id)config)
{ {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
UIView *view = viewRegistry[reactTag]; UIView *view = viewRegistry[reactTag];
...@@ -37,7 +38,7 @@ RCT_EXPORT_METHOD(capture: (nonnull NSNumber *)reactTag) ...@@ -37,7 +38,7 @@ RCT_EXPORT_METHOD(capture: (nonnull NSNumber *)reactTag)
} }
else { else {
GLCanvas *glCanvas = (GLCanvas *)view; GLCanvas *glCanvas = (GLCanvas *)view;
[glCanvas requestCaptureFrame]; [glCanvas requestCaptureFrame:[RCTConvert CaptureConfig:config]];
} }
}]; }];
} }
......
#import "RCTConvert.h"
#import "CaptureConfig.h"
@interface RCTConvert (CaptureConfig)
+ (CaptureConfig *)CaptureConfig:(id)json;
@end
//
// RCTConvert+CaptureConfig.m
// RNGL
//
// Created by Gaetan Renaudeau on 30/12/15.
//
//
#import "RCTConvert+CaptureConfig.h"
@implementation RCTConvert (CaptureConfig)
+ (CaptureConfig *)CaptureConfig:(id)json
{
return [[CaptureConfig alloc]
initWithFormat:[RCTConvert NSString:json[@"format"]]
withType:[RCTConvert NSString:json[@"type"]]
withQuality:[RCTConvert NSNumber:json[@"quality"]]
withFilePath:[RCTConvert NSString:json[@"filePath"]]];
}
@end
...@@ -18,6 +18,8 @@ ...@@ -18,6 +18,8 @@
346089D81BEFD0A500C90DB5 /* GLTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 346089CB1BEFD0A500C90DB5 /* GLTexture.m */; }; 346089D81BEFD0A500C90DB5 /* GLTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 346089CB1BEFD0A500C90DB5 /* GLTexture.m */; };
346089D91BEFD0A500C90DB5 /* RCTConvert+GLData.m in Sources */ = {isa = PBXBuildFile; fileRef = 346089CD1BEFD0A500C90DB5 /* RCTConvert+GLData.m */; }; 346089D91BEFD0A500C90DB5 /* RCTConvert+GLData.m in Sources */ = {isa = PBXBuildFile; fileRef = 346089CD1BEFD0A500C90DB5 /* RCTConvert+GLData.m */; };
346089DA1BEFD0A500C90DB5 /* RNGLContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 346089CF1BEFD0A500C90DB5 /* RNGLContext.m */; }; 346089DA1BEFD0A500C90DB5 /* RNGLContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 346089CF1BEFD0A500C90DB5 /* RNGLContext.m */; };
34C990421C34939C002F49FC /* RCTConvert+CaptureConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C990411C34939C002F49FC /* RCTConvert+CaptureConfig.m */; };
34C990471C349422002F49FC /* CaptureConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C990461C349422002F49FC /* CaptureConfig.m */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
...@@ -55,6 +57,10 @@ ...@@ -55,6 +57,10 @@
346089CD1BEFD0A500C90DB5 /* RCTConvert+GLData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+GLData.m"; sourceTree = "<group>"; }; 346089CD1BEFD0A500C90DB5 /* RCTConvert+GLData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+GLData.m"; sourceTree = "<group>"; };
346089CE1BEFD0A500C90DB5 /* RNGLContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNGLContext.h; sourceTree = "<group>"; }; 346089CE1BEFD0A500C90DB5 /* RNGLContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNGLContext.h; sourceTree = "<group>"; };
346089CF1BEFD0A500C90DB5 /* RNGLContext.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RNGLContext.m; sourceTree = "<group>"; tabWidth = 2; }; 346089CF1BEFD0A500C90DB5 /* RNGLContext.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RNGLContext.m; sourceTree = "<group>"; tabWidth = 2; };
34C990401C34939C002F49FC /* RCTConvert+CaptureConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+CaptureConfig.h"; sourceTree = "<group>"; };
34C990411C34939C002F49FC /* RCTConvert+CaptureConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+CaptureConfig.m"; sourceTree = "<group>"; tabWidth = 2; };
34C990451C34941C002F49FC /* CaptureConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CaptureConfig.h; sourceTree = "<group>"; };
34C990461C349422002F49FC /* CaptureConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = CaptureConfig.m; sourceTree = "<group>"; tabWidth = 2; };
4107012F1ACB723B00C6AA39 /* libRNGL.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNGL.a; sourceTree = BUILT_PRODUCTS_DIR; }; 4107012F1ACB723B00C6AA39 /* libRNGL.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNGL.a; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
...@@ -92,6 +98,10 @@ ...@@ -92,6 +98,10 @@
346089CB1BEFD0A500C90DB5 /* GLTexture.m */, 346089CB1BEFD0A500C90DB5 /* GLTexture.m */,
346089CC1BEFD0A500C90DB5 /* RCTConvert+GLData.h */, 346089CC1BEFD0A500C90DB5 /* RCTConvert+GLData.h */,
346089CD1BEFD0A500C90DB5 /* RCTConvert+GLData.m */, 346089CD1BEFD0A500C90DB5 /* RCTConvert+GLData.m */,
34C990461C349422002F49FC /* CaptureConfig.m */,
34C990451C34941C002F49FC /* CaptureConfig.h */,
34C990401C34939C002F49FC /* RCTConvert+CaptureConfig.h */,
34C990411C34939C002F49FC /* RCTConvert+CaptureConfig.m */,
346089CE1BEFD0A500C90DB5 /* RNGLContext.h */, 346089CE1BEFD0A500C90DB5 /* RNGLContext.h */,
346089CF1BEFD0A500C90DB5 /* RNGLContext.m */, 346089CF1BEFD0A500C90DB5 /* RNGLContext.m */,
410701301ACB723B00C6AA39 /* Products */, 410701301ACB723B00C6AA39 /* Products */,
...@@ -161,10 +171,12 @@ ...@@ -161,10 +171,12 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
34C990421C34939C002F49FC /* RCTConvert+CaptureConfig.m in Sources */,
346089D31BEFD0A500C90DB5 /* GLFBO.m in Sources */, 346089D31BEFD0A500C90DB5 /* GLFBO.m in Sources */,
346089DA1BEFD0A500C90DB5 /* RNGLContext.m in Sources */, 346089DA1BEFD0A500C90DB5 /* RNGLContext.m in Sources */,
346089D21BEFD0A500C90DB5 /* GLData.m in Sources */, 346089D21BEFD0A500C90DB5 /* GLData.m in Sources */,
346089D91BEFD0A500C90DB5 /* RCTConvert+GLData.m in Sources */, 346089D91BEFD0A500C90DB5 /* RCTConvert+GLData.m in Sources */,
34C990471C349422002F49FC /* CaptureConfig.m in Sources */,
346089D51BEFD0A500C90DB5 /* GLImageData.m in Sources */, 346089D51BEFD0A500C90DB5 /* GLImageData.m in Sources */,
346089D41BEFD0A500C90DB5 /* GLImage.m in Sources */, 346089D41BEFD0A500C90DB5 /* GLImage.m in Sources */,
346089D61BEFD0A500C90DB5 /* GLRenderData.m in Sources */, 346089D61BEFD0A500C90DB5 /* GLRenderData.m in Sources */,
......
...@@ -12,4 +12,4 @@ See README install instructions. ...@@ -12,4 +12,4 @@ See README install instructions.
React.NativeModules.UIManager.GLCanvas is %s`, GLCanvas); React.NativeModules.UIManager.GLCanvas is %s`, GLCanvas);
const {Commands} = GLCanvas; const {Commands} = GLCanvas;
module.exports = handle => UIManager.dispatchViewManagerCommand(handle, Commands.captureFrame, []); module.exports = (handle, config) => UIManager.dispatchViewManagerCommand(handle, Commands.captureFrame, [ config ]);
...@@ -10,4 +10,4 @@ See README install instructions. ...@@ -10,4 +10,4 @@ See README install instructions.
React.NativeModules.GLCanvasManager is %s`, GLCanvasManager); React.NativeModules.GLCanvasManager is %s`, GLCanvasManager);
module.exports = handle => GLCanvasManager.capture(handle); module.exports = (handle, config) => GLCanvasManager.capture(handle, config);
const invariant = require("invariant");
const React = require("react-native"); const React = require("react-native");
const { const {
Component, Component,
...@@ -6,6 +7,9 @@ const { ...@@ -6,6 +7,9 @@ const {
const captureFrame = require("./GLCanvas.captureFrame"); const captureFrame = require("./GLCanvas.captureFrame");
const serializeOption = config =>
config.format + ":" + config.type + ":" + config.quality;
const GLCanvasNative = requireNativeComponent("GLCanvas", GLCanvas, { const GLCanvasNative = requireNativeComponent("GLCanvas", GLCanvas, {
nativeOnly: { nativeOnly: {
onGLChange: true, onGLChange: true,
...@@ -25,21 +29,85 @@ function defer() { ...@@ -25,21 +29,85 @@ function defer() {
} }
class GLCanvas extends Component { class GLCanvas extends Component {
captureFrame (cb) {
const promise = ( componentWillMount () {
this._pendingCaptureFrame || // use pending capture OR create a new captureFrame pending this._pendingCaptureFrame = {};
(captureFrame(React.findNodeHandle(this.refs.native)), this._pendingCaptureFrame = defer()) }
).promise;
if (typeof cb === "function") { componentWillUnmount () {
console.warn("GLSurface: callback parameter of captureFrame is deprecated, use the returned promise instead"); // eslint-disable-line no-console Object.keys(this._pendingCaptureFrame).forEach(key =>
promise.then(cb); this._pendingCaptureFrame[key].reject(new Error("GLCanvas is unmounting")));
this._pendingCaptureFrame = null;
}
addPendingCaptureFrame (config) {
const key = serializeOption(config);
return this._pendingCaptureFrame[key] || (
(captureFrame(React.findNodeHandle(this.refs.native), config),
this._pendingCaptureFrame[key] = defer())
);
}
captureFrame (configArg) {
let config;
if (configArg) {
invariant(typeof configArg==="object", "captureFrame takes an object option in parameter");
let nb = 0;
if ("format" in configArg) {
invariant(
typeof configArg.format === "string",
"captureFrame({format}): format must be a string (e.g: 'base64'), Got: '%s'",
configArg.format);
if (configArg.format === "file") invariant(
typeof configArg.filePath === "string" && configArg.filePath,
"captureFrame({filePath}): filePath must be defined when using 'file' format and be an non-empty string, Got: '%s'",
configArg.filePath);
nb ++;
} }
return promise; if ("type" in configArg) {
invariant(
typeof configArg.type === "string",
"captureFrame({type}): type must be a string (e.g: 'png', 'jpg'), Got: '%s'",
configArg.type);
nb ++;
} }
onGLCaptureFrame = ({ nativeEvent: {frame} }) => { if ("quality" in configArg) {
this._pendingCaptureFrame.resolve(frame); invariant(
this._pendingCaptureFrame = undefined; typeof configArg.quality === "number" &&
configArg.quality >= 0 &&
configArg.quality <= 1,
"captureFrame({quality}): quality must be a number between 0 and 1, Got: '%s'",
configArg.quality);
nb ++;
} }
if ("filePath" in configArg) {
nb ++;
}
const keys = Object.keys(configArg);
invariant(keys.length === nb, "captureFrame(config): config must be an object with {format, type, quality, filePath}, found some invalid keys in '%s'", keys);
config = configArg;
}
return this.addPendingCaptureFrame({
format: "base64",
type: "png",
quality: 1,
filePath: "",
...config
}).promise;
}
onGLCaptureFrame = ({ nativeEvent: { error, result, config } }) => {
const key = serializeOption(config);
invariant(key in this._pendingCaptureFrame, "capture '%s' is not scheduled in this._pendingCaptureFrame", key);
if (error) {
this._pendingCaptureFrame[key].reject(error);
}
else {
this._pendingCaptureFrame[key].resolve(result);
}
delete this._pendingCaptureFrame[key];
}
render () { render () {
const { width, height, onLoad, onProgress, eventsThrough, ...restProps } = this.props; const { width, height, onLoad, onProgress, eventsThrough, ...restProps } = this.props;
return <GLCanvasNative return <GLCanvasNative
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment